@object-ui/data-objectstack 17.3.0 → 17.6.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 (40) hide show
  1. package/CHANGELOG.md +888 -0
  2. package/README.md +3 -11
  3. package/dist/index.cjs +1511 -1115
  4. package/dist/index.d.cts +1222 -824
  5. package/dist/index.d.ts +1222 -824
  6. package/dist/index.js +1503 -1111
  7. package/package.json +5 -6
  8. package/src/aggregate-capability.test.ts +0 -177
  9. package/src/bulkDelete.test.ts +0 -75
  10. package/src/bulkUpdate.test.ts +0 -93
  11. package/src/cache/MetadataCache.test.ts +0 -477
  12. package/src/cache/MetadataCache.ts +0 -289
  13. package/src/cloud.ts +0 -109
  14. package/src/connection.test.ts +0 -188
  15. package/src/contracts.ts +0 -115
  16. package/src/errors.test.ts +0 -426
  17. package/src/errors.ts +0 -293
  18. package/src/exportDownload.test.ts +0 -141
  19. package/src/filter-entry-translation.test.ts +0 -360
  20. package/src/filter-operator-ast-parity.test.ts +0 -71
  21. package/src/getObjectSchema.test.ts +0 -145
  22. package/src/importJob.test.ts +0 -71
  23. package/src/index.ts +0 -3658
  24. package/src/integration.ts +0 -200
  25. package/src/listViews.test.ts +0 -175
  26. package/src/metadata-client.test.ts +0 -303
  27. package/src/metadata-client.ts +0 -849
  28. package/src/occ.test.ts +0 -101
  29. package/src/onMutation.test.ts +0 -562
  30. package/src/onWriteWarning.test.ts +0 -273
  31. package/src/orderby-serialization.test.ts +0 -115
  32. package/src/queryDataset.test.ts +0 -132
  33. package/src/searchAll.test.ts +0 -120
  34. package/src/security.ts +0 -243
  35. package/src/spec-symbol-batch6.test.ts +0 -306
  36. package/src/studio.ts +0 -152
  37. package/src/userState.test.ts +0 -315
  38. package/src/userState.ts +0 -238
  39. package/src/v3-compat.test.ts +0 -240
  40. package/src/validation-error.test.ts +0 -94
package/dist/index.d.cts CHANGED
@@ -1,518 +1,12 @@
1
1
  import { ObjectStackClient } from '@objectstack/client';
2
2
  import { DroppedFieldsEvent } from '@objectstack/spec/data';
3
3
  export { DroppedFieldsEvent } from '@objectstack/spec/data';
4
- import { DataSource, QueryParams, QueryResult, GlobalSearchResult, DataSourceMutationEvent, BatchTransactionOperation, ImportRequestOptions, ImportRecordsResult, CreateImportJobResult, ImportJobProgressInfo, ImportJobResultsInfo, ListImportJobsOptions, ImportJobSummaryInfo, ImportJobUndoResult, ExportDownloadRequest, FileUploadResult } from '@object-ui/types';
5
- export { FileUploadResult } from '@object-ui/types';
6
-
7
- /**
8
- * ObjectUI
9
- * Copyright (c) 2024-present ObjectStack Inc.
10
- *
11
- * This source code is licensed under the MIT license found in the
12
- * LICENSE file in the root directory of this source tree.
13
- */
14
- /**
15
- * Base error class for all ObjectStack adapter errors
16
- */
17
- declare class ObjectStackError extends Error {
18
- code: string;
19
- statusCode?: number | undefined;
20
- details?: Record<string, unknown> | undefined;
21
- /**
22
- * Create a new ObjectStackError
23
- *
24
- * @param message - Human-readable error message
25
- * @param code - Unique error code for programmatic handling
26
- * @param statusCode - Optional HTTP status code
27
- * @param details - Optional additional error details for debugging
28
- */
29
- constructor(message: string, code: string, statusCode?: number | undefined, details?: Record<string, unknown> | undefined);
30
- /**
31
- * Convert error to JSON for logging/debugging
32
- */
33
- toJSON(): {
34
- name: string;
35
- message: string;
36
- code: string;
37
- statusCode: number | undefined;
38
- details: Record<string, unknown> | undefined;
39
- stack: string | undefined;
40
- };
41
- }
42
- /**
43
- * Error thrown when requested metadata/schema is not found
44
- */
45
- declare class MetadataNotFoundError extends ObjectStackError {
46
- constructor(objectName: string, details?: Record<string, unknown>);
47
- }
48
- /**
49
- * Error thrown when a bulk operation fails
50
- */
51
- declare class BulkOperationError extends ObjectStackError {
52
- successCount: number;
53
- failureCount: number;
54
- errors: Array<{
55
- index: number;
56
- error: unknown;
57
- }>;
58
- /**
59
- * Create a new BulkOperationError
60
- *
61
- * @param operation - The bulk operation that failed (create, update, delete)
62
- * @param successCount - Number of successful operations
63
- * @param failureCount - Number of failed operations
64
- * @param errors - Array of individual errors
65
- * @param details - Additional error details
66
- */
67
- constructor(operation: 'create' | 'update' | 'delete', successCount: number, failureCount: number, errors: Array<{
68
- index: number;
69
- error: unknown;
70
- }>, details?: Record<string, unknown>);
71
- /**
72
- * Get a summary of the bulk operation failure
73
- */
74
- getSummary(): {
75
- operation: string;
76
- total: number;
77
- successful: number;
78
- failed: number;
79
- failureRate: number;
80
- errors: {
81
- index: number;
82
- error: unknown;
83
- }[];
84
- };
85
- }
86
- /**
87
- * Error thrown when connection to ObjectStack server fails
88
- */
89
- declare class ConnectionError extends ObjectStackError {
90
- url?: string | undefined;
91
- constructor(message: string, url?: string | undefined, details?: Record<string, unknown>, statusCode?: number);
92
- }
93
- /**
94
- * Error thrown when authentication fails
95
- */
96
- declare class AuthenticationError extends ObjectStackError {
97
- constructor(message?: string, details?: Record<string, unknown>, statusCode?: number);
98
- }
99
- /**
100
- * Error thrown when the ObjectStack data API rejects a write as invalid.
101
- *
102
- * NOT the spec's `ValidationError` (`@objectstack/spec/kernel`), whose name this
103
- * class wore until objectui#3160 (objectstack#4115 ledger batch 6). That one is
104
- * a plain DATA SHAPE — `{ field, message, code? }`, one entry in a plugin
105
- * manifest's validation report — and `@object-ui/types` re-exports it under that
106
- * name. This is a runtime `Error` subclass carrying an HTTP status plus a list
107
- * of such entries, so the two are not even the same KIND of thing.
108
- *
109
- * The name follows the convention registered on objectstack#4115 for this
110
- * family — `<what was validated>Validation<Error|Result>`. `@object-ui/core`
111
- * took `SchemaNodeValidationError` for its SDUI-tree walk; this one belongs to
112
- * the data API.
113
- */
114
- declare class DataApiValidationError extends ObjectStackError {
115
- field?: string | undefined;
116
- validationErrors?: Array<{
117
- field: string;
118
- message: string;
119
- }> | undefined;
120
- /**
121
- * Create a new DataApiValidationError
122
- *
123
- * @param message - Human-readable error message
124
- * @param field - The field that failed validation (optional)
125
- * @param validationErrors - Array of validation error details
126
- * @param details - Additional error details
127
- */
128
- constructor(message: string, field?: string | undefined, validationErrors?: Array<{
129
- field: string;
130
- message: string;
131
- }> | undefined, details?: Record<string, unknown>);
132
- /**
133
- * Get all validation errors as a formatted list
134
- */
135
- getValidationErrors(): {
136
- field: string;
137
- message: string;
138
- }[];
139
- }
140
- /**
141
- * Helper function to create an error from an HTTP response
142
- *
143
- * @param response - Response object or error from fetch/axios
144
- * @param context - Additional context for debugging
145
- * @returns Appropriate error instance
146
- */
147
- declare function createErrorFromResponse(response: Record<string, unknown>, context?: string): ObjectStackError;
148
- /**
149
- * Type guard to check if an error is an ObjectStackError
150
- */
151
- declare function isObjectStackError(error: unknown): error is ObjectStackError;
152
- /**
153
- * Type guard to check if an error is a specific ObjectStack error type
154
- */
155
- declare function isErrorType<T extends ObjectStackError>(error: unknown, errorClass: new (...args: any[]) => T): error is T;
156
-
157
- /**
158
- * ObjectUI
159
- * Copyright (c) 2024-present ObjectStack Inc.
160
- *
161
- * This source code is licensed under the MIT license found in the
162
- * LICENSE file in the root directory of this source tree.
163
- */
164
- /**
165
- * Statistics reported by {@link MetadataCache.getStats} for monitoring.
166
- *
167
- * NOT the spec's `CacheStats` (`@objectstack/spec/contracts`), whose name this
168
- * interface wore until objectui#3160 (objectstack#4115 ledger batch 6). That one
169
- * describes the platform's `ICacheService` — a server-side KV cache measured by
170
- * `keyCount` and `memoryUsage`. This one describes the browser-side LRU in front
171
- * of `/api/v1/meta/*`: it is bounded (`size`/`maxSize`), it evicts, it coalesces
172
- * concurrent fetches onto one in-flight promise, and it reports a `hitRate`.
173
- * Neither type has a key the other has, so this is a name collision, not a
174
- * dialect — deriving would have replaced every field.
175
- */
176
- interface MetadataCacheStats {
177
- size: number;
178
- maxSize: number;
179
- hits: number;
180
- misses: number;
181
- evictions: number;
182
- /** Number of concurrent fetches that were coalesced onto an in-flight request. */
183
- coalesced: number;
184
- hitRate: number;
185
- }
186
-
187
- /**
188
- * ObjectUI
189
- * Copyright (c) 2024-present ObjectStack Inc.
190
- *
191
- * This source code is licensed under the MIT license found in the
192
- * LICENSE file in the root directory of this source tree.
193
- */
194
- /**
195
- * Cloud namespace integration for @objectstack/spec v3.0.0
196
- * Replaces the legacy Hub namespace for cloud deployment, hosting, and marketplace schemas.
197
- */
198
- interface CloudDeploymentConfig {
199
- /** Target environment */
200
- environment: 'development' | 'staging' | 'production';
201
- /** Cloud region */
202
- region?: string;
203
- /** Auto-scaling configuration */
204
- scaling?: {
205
- minInstances: number;
206
- maxInstances: number;
207
- targetCPU?: number;
208
- };
209
- /** Environment variables */
210
- envVars?: Record<string, string>;
211
- }
212
- interface CloudHostingConfig {
213
- /** Custom domain */
214
- customDomain?: string;
215
- /** SSL configuration */
216
- ssl?: {
217
- enabled: boolean;
218
- autoRenew: boolean;
219
- };
220
- /** CDN configuration */
221
- cdn?: {
222
- enabled: boolean;
223
- regions?: string[];
224
- };
225
- }
226
- interface CloudMarketplaceEntry {
227
- /** Plugin or app ID */
228
- id: string;
229
- /** Display name */
230
- name: string;
231
- /** Description */
232
- description: string;
233
- /** Version */
234
- version: string;
235
- /** Author */
236
- author: string;
237
- /** Category */
238
- category: string;
239
- /** Tags */
240
- tags: string[];
241
- /** Rating (1-5) */
242
- rating?: number;
243
- /** Install count */
244
- installCount?: number;
245
- /** Price (0 = free) */
246
- price?: number;
247
- }
248
- /**
249
- * Cloud operations helper for ObjectStack adapter.
250
- * Provides methods for cloud deployment, hosting, and marketplace operations.
251
- */
252
- declare class CloudOperations {
253
- private getClient;
254
- constructor(getClient: () => any);
255
- /**
256
- * Deploy an application to the cloud.
257
- */
258
- deploy(appId: string, config: CloudDeploymentConfig): Promise<{
259
- deploymentId: string;
260
- status: string;
261
- }>;
262
- /**
263
- * Get deployment status.
264
- */
265
- getDeploymentStatus(deploymentId: string): Promise<{
266
- status: string;
267
- url?: string;
268
- }>;
269
- /**
270
- * Search marketplace entries.
271
- */
272
- searchMarketplace(query?: string, category?: string): Promise<CloudMarketplaceEntry[]>;
273
- /**
274
- * Install a marketplace plugin.
275
- */
276
- installPlugin(pluginId: string): Promise<{
277
- success: boolean;
278
- }>;
279
- }
280
-
281
- /**
282
- * ObjectUI
283
- * Copyright (c) 2024-present ObjectStack Inc.
284
- *
285
- * This source code is licensed under the MIT license found in the
286
- * LICENSE file in the root directory of this source tree.
287
- */
288
- /**
289
- * Contracts module integration for @objectstack/spec v3.0.0
290
- * Provides plugin contract validation and marketplace publishing utilities.
291
- */
292
- interface PluginContract {
293
- /** Plugin name */
294
- name: string;
295
- /** Plugin version */
296
- version: string;
297
- /** Required peer dependencies */
298
- peerDependencies?: Record<string, string>;
299
- /** Exported component types */
300
- exports: PluginExport[];
301
- /** Required permissions */
302
- permissions?: string[];
303
- /** API surface contract */
304
- api?: PluginAPIContract;
305
- }
306
- interface PluginExport {
307
- /** Export name */
308
- name: string;
309
- /** Export type */
310
- type: 'component' | 'hook' | 'utility' | 'provider';
311
- /** Description */
312
- description?: string;
313
- }
314
- interface PluginAPIContract {
315
- /** Consumed data sources */
316
- dataSources?: string[];
317
- /** Required object schemas */
318
- requiredSchemas?: string[];
319
- /** Event subscriptions */
320
- events?: string[];
321
- }
322
- interface ContractValidationResult {
323
- valid: boolean;
324
- errors: ContractValidationError[];
325
- warnings: string[];
326
- }
327
- interface ContractValidationError {
328
- field: string;
329
- message: string;
330
- code: string;
331
- }
332
- /**
333
- * Validate a plugin contract against the ObjectStack spec.
334
- */
335
- declare function validatePluginContract(contract: PluginContract): ContractValidationResult;
336
- /**
337
- * Generate a plugin contract manifest for marketplace publishing.
338
- */
339
- declare function generateContractManifest(contract: PluginContract): Record<string, unknown>;
340
-
341
- /**
342
- * ObjectStack-backed `UserDataAdapter` factory.
343
- *
344
- * Persists arbitrary per-user UI state (favorites, recently-accessed items, …)
345
- * as a single JSON blob per `(user_id, key)` row in the **unified per-user
346
- * KV store** — the `sys_user_preference` object shipped by every
347
- * `@objectstack/plugin-auth`-enabled environment.
348
- *
349
- * Using the existing `sys_user_preference` table (rather than a parallel
350
- * `user_app_state` table) keeps things consistent with the platform's
351
- * "one KV store per scope" pattern:
352
- *
353
- * - `sys_setting` ← tenant / env scope
354
- * - `sys_user_preference` ← per-user scope ← we live here
355
- *
356
- * Callers are encouraged to namespace their keys (e.g. `ui.favorites`,
357
- * `ui.recent`, `ui.grid.account.state`) so explicit settings (`theme`,
358
- * `locale`) and machine-written UI traces stay easy to tell apart.
359
- *
360
- * ## Schema this adapter expects
361
- *
362
- * The canonical `sys_user_preference` schema (from
363
- * `@objectstack/platform-objects`):
364
- *
365
- * ```yaml
366
- * object: sys_user_preference
367
- * fields:
368
- * user_id: lookup(sys_user) # indexed
369
- * key: string # indexed (e.g. "ui.favorites")
370
- * value: json # the serialised list
371
- * updated_at: datetime # auto-managed
372
- * unique: [user_id, key]
373
- * ```
374
- *
375
- * ## Failure modes
376
- *
377
- * The adapter is designed to **degrade silently**. Any error — missing
378
- * schema, 4xx/5xx, network — is caught:
379
- *
380
- * - `load()` returns `[]`, so the hosting provider keeps its localStorage state.
381
- * - `save()` resolves without throwing, so UI mutations never surface a toast.
382
- *
383
- * As soon as the backend supports `sys_user_preference`, persistence
384
- * "lights up" with no code change.
385
- *
386
- * @module
387
- */
388
-
389
- interface ObjectStackUserStateAdapterOptions {
390
- /** Connected data source (usually the one provided by `<AdapterProvider>`). */
391
- dataSource: DataSource<any>;
392
- /** Authenticated user id. */
393
- userId: string;
394
- /**
395
- * Storage key. Should be a dotted, namespaced string so UI traces
396
- * don't collide with user-facing preferences. Examples:
397
- * `ui.favorites`, `ui.recent`, `ui.grid.account.state`.
398
- */
399
- key: string;
400
- /** Override the storage object name. Defaults to `"sys_user_preference"`. */
401
- resource?: string;
402
- /**
403
- * Optional console logger for development diagnostics. Defaults to noop so
404
- * production builds stay quiet.
405
- */
406
- onError?: (where: 'load' | 'save', error: unknown) => void;
407
- }
408
- interface UserDataAdapter<T> {
409
- load(): Promise<T[]>;
410
- save(items: T[]): Promise<void>;
411
- }
412
- /**
413
- * Build a `UserDataAdapter<T>` backed by ObjectStack.
414
- *
415
- * Each adapter instance is bound to a single `(user, key)` pair; create
416
- * one per slot you want to persist.
417
- */
418
- declare function createObjectStackUserStateAdapter<T = unknown>(options: ObjectStackUserStateAdapterOptions): UserDataAdapter<T>;
419
-
420
- /**
421
- * ObjectUI
422
- * Copyright (c) 2024-present ObjectStack Inc.
423
- *
424
- * This source code is licensed under the MIT license found in the
425
- * LICENSE file in the root directory of this source tree.
426
- */
427
- /**
428
- * Integration module for @objectstack/spec v3.0.0
429
- * Provides third-party service connectors for Slack, email, webhooks.
430
- */
431
- type IntegrationProvider = 'slack' | 'email' | 'webhook' | 'teams' | 'discord';
432
- interface IntegrationConfig {
433
- /** Integration provider type */
434
- provider: IntegrationProvider;
435
- /** Whether this integration is enabled */
436
- enabled: boolean;
437
- /** Provider-specific configuration */
438
- config: Record<string, unknown>;
439
- /** Event triggers */
440
- triggers?: IntegrationTrigger[];
441
- }
442
- interface IntegrationTrigger {
443
- /** Event name (e.g. 'record.created', 'record.updated') */
444
- event: string;
445
- /** Filter condition */
446
- filter?: string;
447
- /** Template for the message/payload */
448
- template?: string;
449
- }
450
- interface SlackIntegrationConfig extends IntegrationConfig {
451
- provider: 'slack';
452
- config: {
453
- webhookUrl: string;
454
- channel?: string;
455
- username?: string;
456
- iconEmoji?: string;
457
- };
458
- }
459
- interface EmailIntegrationConfig extends IntegrationConfig {
460
- provider: 'email';
461
- config: {
462
- smtpHost: string;
463
- smtpPort: number;
464
- secure: boolean;
465
- from: string;
466
- to: string[];
467
- subject?: string;
468
- };
469
- }
470
- interface WebhookIntegrationConfig extends IntegrationConfig {
471
- provider: 'webhook';
472
- config: {
473
- url: string;
474
- method: 'GET' | 'POST' | 'PUT' | 'PATCH';
475
- headers?: Record<string, string>;
476
- retryCount?: number;
477
- retryDelay?: number;
478
- };
479
- }
480
- /**
481
- * Integration service manager.
482
- * Manages third-party service connections and event dispatching.
483
- */
484
- declare class IntegrationManager {
485
- private integrations;
486
- /**
487
- * Register a new integration.
488
- */
489
- register(id: string, config: IntegrationConfig): void;
490
- /**
491
- * Remove an integration.
492
- */
493
- unregister(id: string): void;
494
- /**
495
- * Get all registered integrations.
496
- */
497
- getAll(): Map<string, IntegrationConfig>;
498
- /**
499
- * Get integrations that match a specific event.
500
- */
501
- getForEvent(event: string): IntegrationConfig[];
502
- /**
503
- * Dispatch an event to all matching integrations.
504
- * Returns results for each integration.
505
- */
506
- dispatch(event: string, payload: Record<string, unknown>): Promise<Array<{
507
- id: string;
508
- success: boolean;
509
- error?: string;
510
- }>>;
511
- /**
512
- * Send payload to a specific integration.
513
- */
514
- private send;
515
- }
4
+ import { GetMetaItemLayeredResponse, RuntimeAuthoringIssue } from '@objectstack/spec/api';
5
+ export { RuntimeAuthoringIssue } from '@objectstack/spec/api';
6
+ import { DatasetSelection, AnalyticsResult } from '@objectstack/spec/contracts';
7
+ import { DataSource, QueryParams, QueryResult, GlobalSearchResult, DataSourceMutationEvent, BatchTransactionOperation, ImportRequestOptions, ImportRecordsResult, CreateImportJobResult, ImportJobProgressInfo, ImportJobResultsInfo, ListImportJobsOptions, ImportJobSummaryInfo, ImportJobUndoResult, ExportDownloadRequest, DeleteViewResult, FileUploadResult } from '@object-ui/types';
8
+ export { DeleteViewResult, FileUploadResult, ViewHomeDeleteOutcome } from '@object-ui/types';
9
+ import { DatasetDrillRange } from '@object-ui/core';
516
10
 
517
11
  /**
518
12
  * ObjectUI
@@ -546,6 +40,46 @@ declare class IntegrationManager {
546
40
  * `unknown`/generic `T`) so callers explicitly narrow per-metadata-
547
41
  * type, mirroring the framework's "single Zod source per type" rule.
548
42
  */
43
+
44
+ /**
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.
48
+ *
49
+ * Deliberately the same shape of seam as `ObjectStackAdapter.onWriteWarning`
50
+ * (#3431/#3455): a successful write whose response carries something the author
51
+ * needs to be told, surfaced to the shell as an event so the data layer never
52
+ * imports a toaster. The difference is only which door produced it — that one
53
+ * is record CRUD, this one is the metadata save door.
54
+ */
55
+ interface MetadataSaveAdvisoryEvent {
56
+ /** Metadata type saved (e.g. `'flow'`). */
57
+ type: string;
58
+ /** Item name saved. */
59
+ name: string;
60
+ /**
61
+ * The save mode the call used. `'draft'` writes are **never gated** — the
62
+ * framework returns at `runtime-authoring-gate.ts`'s D1 early-return
63
+ * (`if (args.state !== 'active') return null`), so a draft save produces no
64
+ * findings at all and this event never fires for one. Carried anyway so a
65
+ * consumer reading the event can tell which door it came through.
66
+ */
67
+ mode: 'draft' | 'publish';
68
+ /** The findings. Never empty — the event is not emitted otherwise. */
69
+ advisories: RuntimeAuthoringIssue[];
70
+ }
71
+ /** Event listener type for save-advisory events. */
72
+ type MetadataSaveAdvisoryListener = (event: MetadataSaveAdvisoryEvent) => void;
73
+ /**
74
+ * Read the `advisories` array off a save response, defensively.
75
+ *
76
+ * The server omits the key entirely on a clean save, so `undefined` is the
77
+ * common case and means "nothing to say". Anything that is not an array of
78
+ * objects carrying the six required keys is dropped rather than rendered: a
79
+ * half-shaped finding would print blanks at the author, and this channel must
80
+ * never turn a successful save into noise.
81
+ */
82
+ declare function readSaveAdvisories(body: unknown): RuntimeAuthoringIssue[];
549
83
  interface MetadataClientConfig {
550
84
  /** Base URL of the ObjectStack server (no trailing slash needed). */
551
85
  baseUrl: string;
@@ -569,6 +103,22 @@ interface MetadataClientConfig {
569
103
  * nothing the preview shows is live until Publish.
570
104
  */
571
105
  previewDrafts?: boolean;
106
+ /**
107
+ * 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.
111
+ *
112
+ * Set on the CONFIG rather than exposed as a `subscribe()` method on purpose:
113
+ * console metadata clients are minted per-component by `useMetadataClient`,
114
+ * so there is no long-lived instance to subscribe to — but every one of them
115
+ * is built by the single `createConsoleMetadataClient` factory, which is
116
+ * where this gets wired exactly once for every save call site in the app.
117
+ *
118
+ * A throw from this callback is swallowed — an advisory channel must never
119
+ * be able to fail a save that the server already committed.
120
+ */
121
+ onSaveAdvisory?: MetadataSaveAdvisoryListener;
572
122
  }
573
123
  interface MetadataListOptions {
574
124
  /** Filter by source package id (matches the `package` query param). */
@@ -596,9 +146,9 @@ interface MetadataDraftHeader {
596
146
  * metadata item to a FILE — `format: json|yaml|ts`, `path`, `indent`,
597
147
  * `prettify`, `sortKeys`, `backup`, `atomic`, `loader`. Not one of those keys
598
148
  * exists here, and not one of these exists there: this is the REST client's
599
- * request envelope — optimistic concurrency (`ifMatch` → `If-Match`), actor
600
- * attribution, the destructive-change override, the ADR-0033 draft/publish
601
- * mode, and the owning package. Same words, different layer.
149
+ * request envelope — optimistic concurrency (`ifMatch` → `If-Match`), the
150
+ * destructive-change override, the ADR-0033 draft/publish mode, and the
151
+ * owning package. Same words, different layer.
602
152
  */
603
153
  interface MetadataClientSaveOptions {
604
154
  /**
@@ -607,8 +157,6 @@ interface MetadataClientSaveOptions {
607
157
  * edits get a 409 instead of overwriting each other.
608
158
  */
609
159
  ifMatch?: string;
610
- /** Optional actor id, sent as `X-Actor` for history attribution. */
611
- actor?: string;
612
160
  /**
613
161
  * Bypass destructive-change protection (Phase 3a). The server returns
614
162
  * `409 destructive_change` with `issues[]` if a write would drop or
@@ -653,14 +201,74 @@ interface MetadataDeleteOptions extends MetadataClientSaveOptions {
653
201
  */
654
202
  state?: 'active' | 'draft';
655
203
  }
656
- /** Layered view of a metadata item — Phase 3a `?layers=true`. */
204
+ /**
205
+ * Which layer an overlay was saved at — `'org'` for a tenant overlay, `'env'`
206
+ * for an environment-level one, `null` exactly when `overlay` is null.
207
+ *
208
+ * DERIVED from `@objectstack/spec`, not restated: the vocabulary lives once, in
209
+ * `GetMetaItemLayeredResponseSchema`'s `overlayScope`
210
+ * (`z.enum(['org', 'env']).nullable()`), and this alias indexes the published
211
+ * response type so a scope the spec adds arrives here with no edit. Restating
212
+ * the union locally is the fork `scripts/check-spec-symbol-derivation.mjs`
213
+ * exists to reject, and the mirror-image mistake — a consumer re-spelling a
214
+ * producer's enum — is what this field carried until objectui#4982.
215
+ *
216
+ * What it carried: `string | null`, under a comment naming the vocabulary as
217
+ * `organization | environment | package`. Not one of those three spellings is a
218
+ * value the producer emits (`metadata-protocol`'s two assignment sites write
219
+ * `'org'` / `'env'`); the schema rejects all three by name, and `package` is
220
+ * not a scope this field has ever had. Because the declared type was `string`
221
+ * the compiler had no opinion, so the wrong comment was the only description of
222
+ * the vocabulary — a planted premise rather than stale prose, and the reason
223
+ * the Studio's layer badge shipped the raw value straight to screen.
224
+ */
225
+ type MetadataOverlayScope = GetMetaItemLayeredResponse['overlayScope'];
226
+ /**
227
+ * ADR-0010 §3.6 — the four-state metadata protection lock
228
+ * (`none` / `no-overlay` / `no-delete` / `full`), read from
229
+ * `GetMetaItemLayeredResponseSchema`'s own `z.enum` rather than spelled out
230
+ * again here (objectui#5024).
231
+ *
232
+ * It used to be written out twice in this file, 42 lines apart: once as
233
+ * {@link MetadataLayered.lock} (optional) and once as
234
+ * {@link MetadataAuditEntry.lockState} (nullable), identical in every other
235
+ * respect and compared by no gate. A fifth state added to one would have left
236
+ * the other compiling — the same "declared N times, diffed by nothing" failure
237
+ * objectui#4972 and objectui#4984 record for other vocabularies.
238
+ *
239
+ * Deriving beats a local alias the two merely share. The producer of these
240
+ * values is the framework, and `packages/spec` already declares the vocabulary,
241
+ * so the copies were restating a schema that existed rather than filling a gap.
242
+ * The card that reported this recorded the opposite — "`@objectstack/spec` 也没
243
+ * 有对应的 `z.enum` 可派生" — and so did the audit panel's neighbouring comment;
244
+ * both predate the enum, which ships in `@objectstack/spec` 17.1.0. This is the
245
+ * same treatment {@link MetadataOverlayScope} above already gets, and it closes
246
+ * the cross-repo half of the drift, not just the in-repo half.
247
+ *
248
+ * ⚠️ This types what this repo may WRITE. It does NOT constrain what a server
249
+ * may SEND: {@link MetadataClient.layered} casts the wire value through
250
+ * unchecked, so every reader must still handle a value outside these four. The
251
+ * lock banner in `ResourceEditPage` is the worked example.
252
+ */
253
+ type MetadataLockState = GetMetaItemLayeredResponse['lock'];
254
+ /**
255
+ * Layered view of a metadata item — the body of
256
+ * `GET /meta/:type/:name/layers` (`GetMetaItemLayeredResponseSchema`).
257
+ *
258
+ * A subset by design: the response also carries `type` / `name`, which the
259
+ * caller already knows because it passed them in, so
260
+ * {@link MetadataClient.layered} does not hand them back.
261
+ */
657
262
  interface MetadataLayered<T = unknown> {
658
263
  /** Code-level (artifact) item; null if the item only exists as an overlay. */
659
264
  code: T | null;
660
265
  /** Org/environment overlay (just the saved delta or full overlay row). */
661
266
  overlay: T | null;
662
- /** Overlay scope (`organization` | `environment` | `package` | null). */
663
- overlayScope: string | null;
267
+ /**
268
+ * Which layer {@link MetadataLayered.overlay} came from, or null when there
269
+ * is no overlay. Spec-derived — see {@link MetadataOverlayScope}.
270
+ */
271
+ overlayScope: MetadataOverlayScope;
664
272
  /** Merged effective view — what the runtime actually sees. */
665
273
  effective: T | null;
666
274
  /**
@@ -672,7 +280,7 @@ interface MetadataLayered<T = unknown> {
672
280
  */
673
281
  _diagnostics?: MetadataDiagnostics;
674
282
  /** 4-state lock: `none` / `no-overlay` / `no-delete` / `full`. */
675
- lock?: 'none' | 'no-overlay' | 'no-delete' | 'full';
283
+ lock?: MetadataLockState;
676
284
  /** Human-readable reason for the lock (tooltip text). */
677
285
  lockReason?: string;
678
286
  /** Which layer set the lock: artifact / package / overlay / env-forced. */
@@ -702,7 +310,10 @@ interface MetadataAuditEntry {
702
310
  id: unknown;
703
311
  /** ISO timestamp when the attempt happened. */
704
312
  occurredAt: string;
705
- /** Who attempted the operation (`x-actor` header or `'system'`). */
313
+ /**
314
+ * Who attempted the operation — the identity the request was authorized
315
+ * as, or `'system'` for internal machine writes (objectstack#7941).
316
+ */
706
317
  actor: string;
707
318
  /** Code path that recorded the row (e.g. `protocol.saveMetaItem`). */
708
319
  source: string | null;
@@ -713,7 +324,7 @@ interface MetadataAuditEntry {
713
324
  /** Machine-readable reason code (`item_locked`, `ok`, …). */
714
325
  code: string;
715
326
  /** Effective lock at the moment of the attempt. */
716
- lockState: 'none' | 'no-overlay' | 'no-delete' | 'full' | null;
327
+ lockState: MetadataLockState | null;
717
328
  /** True when admin forced the write through despite the lock. */
718
329
  lockOverridden: boolean;
719
330
  /** Request-id for trace correlation (if propagated). */
@@ -832,6 +443,8 @@ declare class MetadataClient {
832
443
  private readonly headers;
833
444
  /** ADR-0037: when true, reads render the draft-overlaid world. */
834
445
  readonly previewDrafts: boolean;
446
+ /** #4133 — sink for post-save advisory findings; see the config field. */
447
+ private readonly onSaveAdvisory;
835
448
  constructor(config: MetadataClientConfig);
836
449
  /** Update the client's environment scope at runtime. */
837
450
  withEnvironment(environmentId: string | undefined): MetadataClient;
@@ -858,9 +471,39 @@ declare class MetadataClient {
858
471
  type?: string;
859
472
  }): Promise<MetadataDraftHeader[]>;
860
473
  /**
861
- * Get a single metadata item. Returns the unwrapped item content
862
- * (matching the framework REST handler which calls `res.json(item)`).
863
- * Returns `null` on 404 to keep the call-site ergonomic.
474
+ * Fetch a single metadata item and return the response body EXACTLY as the
475
+ * server sent it — envelope and all.
476
+ *
477
+ * The one transport both {@link get} (which unwraps) and {@link getDraft}
478
+ * (which does not) sit on, so the two can differ in what they hand back
479
+ * without differing in how they ask. Private on purpose: the envelope is a
480
+ * wire detail, and the two published contracts above are the supported ways
481
+ * to read an item.
482
+ */
483
+ private readItemResponse;
484
+ /**
485
+ * Get a single metadata item. Returns the unwrapped item CONTENT — the
486
+ * metadata document itself, so `obj.fields` / `obj.label` are reachable
487
+ * directly.
488
+ *
489
+ * The framework answers `GET /meta/:type/:name` with the spec-declared
490
+ * envelope `{ type, name, item, …protection fields }`
491
+ * (`GetMetaItemResponseSchema`; objectstack#5563 collapsed this read to that
492
+ * ONE shape), so the envelope is unwrapped here — at the client boundary,
493
+ * once — rather than by each caller.
494
+ *
495
+ * **This method used to return the envelope while promising the body**
496
+ * (objectui#4271). Nothing detected the disagreement, because the test
497
+ * doubles across the repo were written against this docblock: every consumer
498
+ * reading `obj.fields` got `undefined` in production and a field list in
499
+ * unit tests. The visible cost was the entire field half of the permission
500
+ * matrix reporting "No fields registered for this object." for every object,
501
+ * plus dead RLS CEL autocomplete, an inert report drill-down and a designer
502
+ * that saved the envelope back over the object body.
503
+ *
504
+ * A response that is NOT the envelope (an older server answering the bare
505
+ * document, or a body of its own shape) passes through untouched, and `null`
506
+ * still comes back on 404 to keep the call site ergonomic.
864
507
  */
865
508
  get<T = unknown>(type: string, name: string, options?: MetadataGetOptions): Promise<T | null>;
866
509
  /**
@@ -869,11 +512,13 @@ declare class MetadataClient {
869
512
  * back to the published overlay or the artifact registry — a `null`
870
513
  * unambiguously means "nothing to publish".
871
514
  *
872
- * Note: the framework wraps draft responses in an envelope
873
- * `{ type, name, item }` (matching `getMetaItem`); callers should
874
- * read `.item` to get the body. The legacy `get()` returns the
875
- * unwrapped body, so this method preserves that asymmetry by
876
- * returning whatever the server sent.
515
+ * Note: this method hands back the `{ type, name, item }` envelope the
516
+ * framework sends, NOT the body — callers read `.item`. That asymmetry with
517
+ * {@link get} is deliberate and long-standing (the draft envelope's identity
518
+ * and protection carriers are part of what a draft reader inspects), so it
519
+ * is preserved by reading the transport directly instead of going through
520
+ * `get()`'s unwrap. `unwrapDraftBody` (app-shell) and `unwrapViewDraft`
521
+ * (this package) are the shared helpers for taking the body out.
877
522
  */
878
523
  getDraft<T = unknown>(type: string, name: string, options?: {
879
524
  packageId?: string;
@@ -908,9 +553,35 @@ declare class MetadataClient {
908
553
  };
909
554
  } & Record<string, unknown>>;
910
555
  /**
911
- * Get the 3-state layered view of a metadata item (Phase 3a). Returns
912
- * `code` (the artifact / fallback default), `overlay` (the saved
913
- * customisation, if any), and `effective` (what the runtime sees).
556
+ * Get the 3-state layered view of a metadata item: `code` (the packaged
557
+ * artifact baseline), `overlay` (the tenant customisation row alone) and
558
+ * `effective` (the merged value the runtime sees).
559
+ *
560
+ * Reads **`GET /meta/:type/:name/layers`** — the path the framework declares
561
+ * for this projection, with a response schema of its own
562
+ * (`GetMetaItemLayeredResponseSchema`, objectstack#5882 ruling B). It used to
563
+ * be reached by hanging a `layers` flag on the ordinary item read, which made
564
+ * one route answer two unrelated representations while `packages/spec`
565
+ * declared only one of them. That spelling still answers this same body
566
+ * inside its deprecation window (the response carries RFC 9745
567
+ * `Deprecation: true` and an RFC 8288 `Link: rel="successor-version"` back to
568
+ * this path) and is scheduled for removal upstream, so nothing here may
569
+ * depend on it — the repo-wide ratchet is
570
+ * `scripts/__tests__/layered-read-declared-path-4016.test.ts`.
571
+ *
572
+ * The request is built here rather than delegated to `@objectstack/client`
573
+ * because the SDK expresses no layered read in EITHER spelling: the
574
+ * framework's REST route ledger records this route as `server-only`,
575
+ * "consumed by objectui over plain HTTP", and whether the SDK should express
576
+ * it is an open upstream product call.
577
+ *
578
+ * One behaviour delta rides along with the path, and it is the server's
579
+ * choice rather than ours: the retired flag FELL THROUGH to the plain item
580
+ * read on a backend whose protocol implementation had no layered support,
581
+ * answering the `{ type, name, item }` envelope. A dedicated path refuses to
582
+ * answer a different resource under this one's declared shape, so it returns
583
+ * 501 `NOT_IMPLEMENTED` instead — which surfaces here as a thrown error
584
+ * rather than a view with `code` and `overlay` silently blank.
914
585
  */
915
586
  layered<T = unknown>(type: string, name: string, options?: {
916
587
  packageId?: string;
@@ -946,7 +617,6 @@ declare class MetadataClient {
946
617
  * overlay moved while the draft was sitting.
947
618
  */
948
619
  publish<T = unknown>(type: string, name: string, options?: {
949
- actor?: string;
950
620
  message?: string;
951
621
  }): Promise<T>;
952
622
  /**
@@ -958,7 +628,6 @@ declare class MetadataClient {
958
628
  * when the target version is a delete tombstone.
959
629
  */
960
630
  rollback<T = unknown>(type: string, name: string, toVersion: number, options?: {
961
- actor?: string;
962
631
  message?: string;
963
632
  }): Promise<T>;
964
633
  /**
@@ -991,126 +660,147 @@ declare class MetadataClient {
991
660
  * LICENSE file in the root directory of this source tree.
992
661
  */
993
662
  /**
994
- * Security module integration for @objectstack/spec v3.0.0
995
- * Provides advanced security policies: CSP config, audit logging, data masking.
663
+ * Base error class for all ObjectStack adapter errors
996
664
  */
665
+ declare class ObjectStackError extends Error {
666
+ code: string;
667
+ statusCode?: number | undefined;
668
+ details?: Record<string, unknown> | undefined;
669
+ /**
670
+ * Create a new ObjectStackError
671
+ *
672
+ * @param message - Human-readable error message
673
+ * @param code - Unique error code for programmatic handling
674
+ * @param statusCode - Optional HTTP status code
675
+ * @param details - Optional additional error details for debugging
676
+ */
677
+ constructor(message: string, code: string, statusCode?: number | undefined, details?: Record<string, unknown> | undefined);
678
+ /**
679
+ * Convert error to JSON for logging/debugging
680
+ */
681
+ toJSON(): {
682
+ name: string;
683
+ message: string;
684
+ code: string;
685
+ statusCode: number | undefined;
686
+ details: Record<string, unknown> | undefined;
687
+ stack: string | undefined;
688
+ };
689
+ }
997
690
  /**
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.
1001
- *
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.
691
+ * Error thrown when requested metadata/schema is not found
1009
692
  */
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;
693
+ declare class MetadataNotFoundError extends ObjectStackError {
694
+ constructor(objectName: string, details?: Record<string, unknown>);
1033
695
  }
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';
696
+ /**
697
+ * Error thrown when a bulk operation fails
698
+ */
699
+ declare class BulkOperationError extends ObjectStackError {
700
+ successCount: number;
701
+ failureCount: number;
702
+ errors: Array<{
703
+ index: number;
704
+ error: unknown;
705
+ }>;
706
+ /**
707
+ * Create a new BulkOperationError
708
+ *
709
+ * @param operation - The bulk operation that failed (create, update, delete)
710
+ * @param successCount - Number of successful operations
711
+ * @param failureCount - Number of failed operations
712
+ * @param errors - Array of individual errors
713
+ * @param details - Additional error details
714
+ */
715
+ constructor(operation: 'create' | 'update' | 'delete', successCount: number, failureCount: number, errors: Array<{
716
+ index: number;
717
+ error: unknown;
718
+ }>, details?: Record<string, unknown>);
719
+ /**
720
+ * Get a summary of the bulk operation failure
721
+ */
722
+ getSummary(): {
723
+ operation: string;
724
+ total: number;
725
+ successful: number;
726
+ failed: number;
727
+ failureRate: number;
728
+ errors: {
729
+ index: number;
730
+ error: unknown;
731
+ }[];
732
+ };
1043
733
  }
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;
734
+ /**
735
+ * Error thrown when connection to ObjectStack server fails
736
+ */
737
+ declare class ConnectionError extends ObjectStackError {
738
+ url?: string | undefined;
739
+ constructor(message: string, url?: string | undefined, details?: Record<string, unknown>, statusCode?: number);
1050
740
  }
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[];
741
+ /**
742
+ * Error thrown when authentication fails
743
+ */
744
+ declare class AuthenticationError extends ObjectStackError {
745
+ constructor(message?: string, details?: Record<string, unknown>, statusCode?: number);
1060
746
  }
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;
747
+ /**
748
+ * Error thrown when the ObjectStack data API rejects a write as invalid.
749
+ *
750
+ * NOT the spec's `ValidationError` (`@objectstack/spec/kernel`), whose name this
751
+ * class wore until objectui#3160 (objectstack#4115 ledger batch 6). That one is
752
+ * a plain DATA SHAPE — `{ field, message, code? }`, one entry in a plugin
753
+ * manifest's validation report — and `@object-ui/types` re-exports it under that
754
+ * name. This is a runtime `Error` subclass carrying an HTTP status plus a list
755
+ * of such entries, so the two are not even the same KIND of thing.
756
+ *
757
+ * The name follows the convention registered on objectstack#4115 for this
758
+ * family — `<what was validated>Validation<Error|Result>`. `@object-ui/core`
759
+ * took `SchemaNodeValidationError` for its SDUI-tree walk; this one belongs to
760
+ * the data API.
761
+ */
762
+ declare class DataApiValidationError extends ObjectStackError {
763
+ field?: string | undefined;
764
+ validationErrors?: Array<{
765
+ field: string;
766
+ message: string;
767
+ }> | undefined;
768
+ /**
769
+ * Create a new DataApiValidationError
770
+ *
771
+ * @param message - Human-readable error message
772
+ * @param field - The field that failed validation (optional)
773
+ * @param validationErrors - Array of validation error details
774
+ * @param details - Additional error details
775
+ */
776
+ constructor(message: string, field?: string | undefined, validationErrors?: Array<{
777
+ field: string;
778
+ message: string;
779
+ }> | undefined, details?: Record<string, unknown>);
780
+ /**
781
+ * Get all validation errors as a formatted list
782
+ */
783
+ getValidationErrors(): {
784
+ field: string;
785
+ message: string;
786
+ }[];
1076
787
  }
1077
788
  /**
1078
- * Security policy manager.
1079
- * Handles CSP generation, audit logging, and data masking.
789
+ * Helper function to create an error from an HTTP response
790
+ *
791
+ * @param response - Response object or error from fetch/axios
792
+ * @param context - Additional context for debugging
793
+ * @returns Appropriate error instance
794
+ */
795
+ declare function createErrorFromResponse(response: Record<string, unknown>, context?: string): ObjectStackError;
796
+ /**
797
+ * Type guard to check if an error is an ObjectStackError
798
+ */
799
+ declare function isObjectStackError(error: unknown): error is ObjectStackError;
800
+ /**
801
+ * Type guard to check if an error is a specific ObjectStack error type
1080
802
  */
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
- }
803
+ declare function isErrorType<T extends ObjectStackError>(error: unknown, errorClass: new (...args: any[]) => T): error is T;
1114
804
 
1115
805
  /**
1116
806
  * ObjectUI
@@ -1120,109 +810,106 @@ declare class SecurityManager {
1120
810
  * LICENSE file in the root directory of this source tree.
1121
811
  */
1122
812
  /**
1123
- * Studio module integration for @objectstack/spec v3.0.0
1124
- * Provides visual designer schema improvements: canvas, property editors, theme builder.
813
+ * Statistics reported by {@link MetadataCache.getStats} for monitoring.
814
+ *
815
+ * NOT the spec's `CacheStats` (`@objectstack/spec/contracts`), whose name this
816
+ * interface wore until objectui#3160 (objectstack#4115 ledger batch 6). That one
817
+ * describes the platform's `ICacheService` — a server-side KV cache measured by
818
+ * `keyCount` and `memoryUsage`. This one describes the browser-side LRU in front
819
+ * of `/api/v1/meta/*`: it is bounded (`size`/`maxSize`), it evicts, it coalesces
820
+ * concurrent fetches onto one in-flight promise, and it reports a `hitRate`.
821
+ * Neither type has a key the other has, so this is a name collision, not a
822
+ * dialect — deriving would have replaced every field.
1125
823
  */
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;
824
+ interface MetadataCacheStats {
825
+ size: number;
826
+ maxSize: number;
827
+ hits: number;
828
+ misses: number;
829
+ evictions: number;
830
+ /** Number of concurrent fetches that were coalesced onto an in-flight request. */
831
+ coalesced: number;
832
+ hitRate: number;
1201
833
  }
834
+
1202
835
  /**
1203
- * Default canvas configuration for designers.
1204
- */
1205
- declare function createDefaultCanvasConfig(overrides?: Partial<StudioCanvasConfig>): StudioCanvasConfig;
1206
- /**
1207
- * Snap a position to the grid.
836
+ * ObjectStack-backed `UserDataAdapter` factory.
837
+ *
838
+ * Persists arbitrary per-user UI state (favorites, recently-accessed items, …)
839
+ * as a single JSON blob per `(user_id, key)` row in the **unified per-user
840
+ * KV store** — the `sys_user_preference` object shipped by every
841
+ * `@objectstack/plugin-auth`-enabled environment.
842
+ *
843
+ * Using the existing `sys_user_preference` table (rather than a parallel
844
+ * `user_app_state` table) keeps things consistent with the platform's
845
+ * "one KV store per scope" pattern:
846
+ *
847
+ * - `sys_setting` ← tenant / env scope
848
+ * - `sys_user_preference` ← per-user scope ← we live here
849
+ *
850
+ * Callers are encouraged to namespace their keys (e.g. `ui.favorites`,
851
+ * `ui.recent`, `ui.grid.account.state`) so explicit settings (`theme`,
852
+ * `locale`) and machine-written UI traces stay easy to tell apart.
853
+ *
854
+ * ## Schema this adapter expects
855
+ *
856
+ * The canonical `sys_user_preference` schema (from
857
+ * `@objectstack/platform-objects`):
858
+ *
859
+ * ```yaml
860
+ * object: sys_user_preference
861
+ * fields:
862
+ * user_id: lookup(sys_user) # indexed
863
+ * key: string # indexed (e.g. "ui.favorites")
864
+ * value: json # the serialised list
865
+ * updated_at: datetime # auto-managed
866
+ * unique: [user_id, key]
867
+ * ```
868
+ *
869
+ * ## Failure modes
870
+ *
871
+ * The adapter is designed to **degrade silently**. Any error — missing
872
+ * schema, 4xx/5xx, network — is caught:
873
+ *
874
+ * - `load()` returns `[]`, so the hosting provider keeps its localStorage state.
875
+ * - `save()` resolves without throwing, so UI mutations never surface a toast.
876
+ *
877
+ * As soon as the backend supports `sys_user_preference`, persistence
878
+ * "lights up" with no code change.
879
+ *
880
+ * @module
1208
881
  */
1209
- declare function snapToGrid(x: number, y: number, gridSize: number): {
1210
- x: number;
1211
- y: number;
1212
- };
882
+
883
+ interface ObjectStackUserStateAdapterOptions {
884
+ /** Connected data source (usually the one provided by `<AdapterProvider>`). */
885
+ dataSource: DataSource<any>;
886
+ /** Authenticated user id. */
887
+ userId: string;
888
+ /**
889
+ * Storage key. Should be a dotted, namespaced string so UI traces
890
+ * don't collide with user-facing preferences. Examples:
891
+ * `ui.favorites`, `ui.recent`, `ui.grid.account.state`.
892
+ */
893
+ key: string;
894
+ /** Override the storage object name. Defaults to `"sys_user_preference"`. */
895
+ resource?: string;
896
+ /**
897
+ * Optional console logger for development diagnostics. Defaults to noop so
898
+ * production builds stay quiet.
899
+ */
900
+ onError?: (where: 'load' | 'save', error: unknown) => void;
901
+ }
902
+ interface UserDataAdapter<T> {
903
+ load(): Promise<T[]>;
904
+ save(items: T[]): Promise<void>;
905
+ }
1213
906
  /**
1214
- * Calculate auto-layout positions for a set of items.
1215
- * Uses a simple grid-based layout algorithm.
907
+ * Build a `UserDataAdapter<T>` backed by ObjectStack.
908
+ *
909
+ * Each adapter instance is bound to a single `(user, key)` pair; create
910
+ * one per slot you want to persist.
1216
911
  */
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
- }>;
912
+ declare function createObjectStackUserStateAdapter<T = unknown>(options: ObjectStackUserStateAdapterOptions): UserDataAdapter<T>;
1226
913
 
1227
914
  /**
1228
915
  * Map human-readable filter operator names produced by SDUI view configs
@@ -1320,6 +1007,83 @@ declare function readTransactionalBatchCapability(discovery: unknown): boolean |
1320
1007
  * tripping on the property-name mismatch.
1321
1008
  */
1322
1009
  declare function is404Error(error: unknown): boolean;
1010
+ /**
1011
+ * The two denials the server derives from an object's `enable` block —
1012
+ * `apiAccessDenialFromEnable` (objectstack `packages/rest/src/rest-server.ts`).
1013
+ *
1014
+ * - `OBJECT_API_DISABLED` (404) — `enable.apiEnabled: false`; the object is
1015
+ * not exposed over the data API at all.
1016
+ * - `OBJECT_API_METHOD_NOT_ALLOWED` (405) — the operation is absent from the
1017
+ * `enable.apiMethods` whitelist.
1018
+ *
1019
+ * Both are **pure functions of the object's metadata**: no user, no permission,
1020
+ * no context, no request body. So neither is transient and neither is
1021
+ * per-user — when one happens it is a permanent property of that object, and
1022
+ * every retry of every persona gets the identical answer.
1023
+ *
1024
+ * That is exactly why they must not be degraded into "no data". A 404 from a
1025
+ * missing collection means *this backend doesn't have that table*, which the
1026
+ * optional-collection probes below legitimately read as "feature unavailable";
1027
+ * a 404 from `OBJECT_API_DISABLED` means *this page can never work*. Answering
1028
+ * the second with an empty result set renders "you have no records" over a
1029
+ * surface that is not allowed to have any (objectui#4408 — it also hid the
1030
+ * upstream defect objectstack#7544 for its entire life, because a merely
1031
+ * unpopulated page invites nobody to click through).
1032
+ */
1033
+ declare const API_ACCESS_DENIED_CODES: readonly ["OBJECT_API_DISABLED", "OBJECT_API_METHOD_NOT_ALLOWED"];
1034
+ /**
1035
+ * True when `error` is an `enable`-block API denial (see
1036
+ * {@link API_ACCESS_DENIED_CODES}).
1037
+ *
1038
+ * Discriminates on the ADR-0112 `code`, never on the status: 404 alone cannot
1039
+ * separate a disabled object from a missing collection, and 405 alone cannot
1040
+ * separate a withheld method from any other method rejection. The code survives
1041
+ * the transport — `@objectstack/client`'s fetch wrapper stamps `error.code`
1042
+ * from the response envelope, and both spellings are declared members of the
1043
+ * spec's `StandardErrorCode` — so no heuristic on status is needed or wanted.
1044
+ */
1045
+ declare function isApiAccessDeniedError(error: unknown): boolean;
1046
+ /**
1047
+ * What the by-name meta app route said about THIS session's access to an app
1048
+ * (objectui#4252 / objectstack#8013).
1049
+ *
1050
+ * - `granted` — the route served the app document.
1051
+ * - `denied` — the app EXISTS and the session lacks its `requiredPermissions`.
1052
+ * The only verdict a caller may render as an authorization refusal.
1053
+ * - `unknown` — anything else: an absent app, an unpublished one, an app
1054
+ * withheld by an absent optional service, an unreachable server, an adapter
1055
+ * that cannot ask. All of these are cases where the server declined to say
1056
+ * that a permission of the caller's is missing, so no caller may claim it.
1057
+ *
1058
+ * Three values rather than a boolean because the third is not a shade of the
1059
+ * other two: "the app is missing" and "I could not find out" both have to leave
1060
+ * the caller's existing copy alone, and collapsing them into `false` invites a
1061
+ * consumer to read a failed probe as a positive absence.
1062
+ */
1063
+ type AppAccessVerdict = 'granted' | 'denied' | 'unknown';
1064
+ /**
1065
+ * The ADR-0112 standard catalog code the by-name meta app route answers with
1066
+ * when an app exists and the session lacks its `requiredPermissions`
1067
+ * (objectstack#8013, `sendError(res, 403, 'PERMISSION_DENIED', …)` in
1068
+ * `packages/rest/src/rest-server.ts`).
1069
+ *
1070
+ * Deliberately NOT a member of {@link API_ACCESS_DENIED_CODES}: those two are
1071
+ * pure functions of an object's `enable` metadata — permanent, identical for
1072
+ * every persona — whereas this one is a statement about the CALLER, and the same
1073
+ * request by a different session succeeds. Same word "denied", different
1074
+ * question, so a consumer that wants one must never match the other.
1075
+ */
1076
+ declare const APP_PERMISSION_DENIED_CODE = "PERMISSION_DENIED";
1077
+ /**
1078
+ * True when `error` is the by-name app route's permission denial.
1079
+ *
1080
+ * Discriminates on the ADR-0112 `code`, never on the status (objectui#4408): the
1081
+ * route answers 403 for this and 404 for absence today, but a status is a
1082
+ * transport fact many conditions share, while the code is the contract. The
1083
+ * console's whole reason to call this is to tell two REFUSALS apart, and both
1084
+ * are errors.
1085
+ */
1086
+ declare function isAppPermissionDeniedError(error: unknown): boolean;
1323
1087
  /**
1324
1088
  * Thrown when the deployment has no analytics capability installed
1325
1089
  * (framework#3891 / #4019).
@@ -1339,10 +1103,97 @@ declare class AnalyticsNotInstalledError extends Error {
1339
1103
  readonly code = "ANALYTICS_NOT_INSTALLED";
1340
1104
  /** The surface that was unavailable, for the message a host renders. */
1341
1105
  readonly surface: string;
1342
- constructor(surface: string, detail?: string);
1106
+ /**
1107
+ * The server's own ADR-0112 code — the field this branch was chosen BY, when
1108
+ * a producer named one (`NOT_IMPLEMENTED` from the mounted route with no
1109
+ * service behind it, `ROUTE_NOT_FOUND` from the dispatcher when the route is
1110
+ * not mounted at all). Absent for the no-code residual, where a bare
1111
+ * transport 404/501 is all there was to read.
1112
+ *
1113
+ * Additive (objectui#5663): carried so a reader can audit that the headline
1114
+ * and the quoted `detail` below came off the same answer.
1115
+ */
1116
+ readonly serverCode?: string;
1117
+ constructor(surface: string, detail?: string, serverCode?: string);
1343
1118
  }
1344
1119
  /** True when `error` is an {@link AnalyticsNotInstalledError} (or its wire twin). */
1345
1120
  declare function isAnalyticsNotInstalledError(error: unknown): boolean;
1121
+ /**
1122
+ * Thrown when the dataset route ANSWERED and said the dataset this query named
1123
+ * does not exist in this environment — `404` + ADR-0112 `NOT_FOUND`, the
1124
+ * `body.datasetName` lookup miss in `@objectstack/rest`'s
1125
+ * `registerAnalyticsEndpoints` (`Dataset "<name>" not found.`).
1126
+ *
1127
+ * A SIBLING of {@link AnalyticsNotInstalledError}, never a shade of it
1128
+ * (objectui#5663). Both conditions answer **404** — routes-not-mounted through
1129
+ * the runtime dispatcher's `ROUTE_NOT_FOUND`, an unknown dataset through this
1130
+ * route's own `NOT_FOUND` — so a `res.status === 404` test cannot separate
1131
+ * them, and the mapping that could not tell them apart reported EVERY unknown
1132
+ * dataset as a missing server capability.
1133
+ *
1134
+ * Measured live on a prod tenant: four HotCRM Executive Overview widgets told
1135
+ * the operator to install `@objectstack/service-analytics` and mount
1136
+ * `AnalyticsServicePlugin`, while the analytics service was installed and
1137
+ * answering the whole time. The real condition was an installed
1138
+ * `app.objectstack.hotcrm` pinned at 1.3.0 whose datasets ship in 2.2.2. The
1139
+ * remedy the banner named — install a server plugin — is the opposite corner of
1140
+ * the system from the remedy that works: upgrade the installed app. A wrong
1141
+ * diagnosis is not a smaller version of no diagnosis; it spends the operator's
1142
+ * time in the wrong subsystem.
1143
+ *
1144
+ * The `code` is the ONLY field that separates the two, which is why this branch
1145
+ * reads it and nothing else. See `readAnalyticsErrorEnvelope` below — the
1146
+ * module-private reader that pulls the code out of either declared envelope.
1147
+ */
1148
+ declare class AnalyticsDatasetNotFoundError extends Error {
1149
+ readonly code = "ANALYTICS_DATASET_NOT_FOUND";
1150
+ /**
1151
+ * The dataset this query asked for, taken from the REQUEST rather than parsed
1152
+ * back out of the server's prose: the request is what we know for certain,
1153
+ * and re-reading the name out of a message would make the headline depend on
1154
+ * that message's WORDING — the coupling framework#5367 spent a whole issue
1155
+ * removing from the producing route.
1156
+ */
1157
+ readonly datasetName?: string;
1158
+ /** The server's own ADR-0112 code — the field this branch was chosen BY. */
1159
+ readonly serverCode?: string;
1160
+ /** The server's own message, quoted verbatim in the parenthetical. */
1161
+ readonly serverMessage?: string;
1162
+ constructor(opts: {
1163
+ datasetName?: string;
1164
+ serverCode?: string;
1165
+ serverMessage?: string;
1166
+ });
1167
+ }
1168
+ /**
1169
+ * Thrown when the dataset query was refused before it ran because the session
1170
+ * is not authenticated — `401` + ADR-0112 `UNAUTHENTICATED`, the REST seam's
1171
+ * `ANONYMOUS_DENY_BODY` (`@objectstack/core`'s `security/anonymous-deny.ts`,
1172
+ * written verbatim by `enforceAuth`).
1173
+ *
1174
+ * The THIRD branch, and the one the reported card under-states (objectui#5663):
1175
+ * it recorded a live `POST /api/v1/analytics/dataset/query -> 401
1176
+ * UNAUTHENTICATED` and read it as evidence for the dataset-unknown branch. It
1177
+ * is neither of the other two. An expired session reported as "the deployment
1178
+ * is missing a capability" is the SAME defect wearing a different mask — an
1179
+ * operator sent to install a server plugin because their token lapsed — so the
1180
+ * separation is the point, not a nicety.
1181
+ *
1182
+ * Deliberately not left to the generic `Dataset query failed: 401 …` either:
1183
+ * that string names a transport status where a person needs an action, and
1184
+ * "sign in again" is an action.
1185
+ */
1186
+ declare class AnalyticsUnauthenticatedError extends Error {
1187
+ readonly code = "ANALYTICS_UNAUTHENTICATED";
1188
+ /** The server's own ADR-0112 code — the field this branch was chosen BY. */
1189
+ readonly serverCode?: string;
1190
+ /** The server's own message, quoted verbatim in the parenthetical. */
1191
+ readonly serverMessage?: string;
1192
+ constructor(opts?: {
1193
+ serverCode?: string;
1194
+ serverMessage?: string;
1195
+ });
1196
+ }
1346
1197
  /**
1347
1198
  * Thrown when the server REJECTED the analytics query body (HTTP 400 —
1348
1199
  * `VALIDATION_FAILED` since framework#4010 validates `/analytics/query` at the
@@ -1365,25 +1216,73 @@ declare class AnalyticsQueryRejectedError extends Error {
1365
1216
  *
1366
1217
  * `@objectstack/client`'s fetch wrapper throws on a non-2xx, decorating the
1367
1218
  * error with the semantic `code` string and the numeric `httpStatus` (the
1368
- * ADR-0112 / framework#3842 shape this repo already reads elsewhere). Two
1369
- * outcomes must NOT be conflated:
1370
- *
1371
- * - **`not-installed`** — the deployment has no analytics service. Since
1372
- * framework#3891 retired the degraded in-kernel shim, that is a 404 (the
1373
- * routes aren't mounted at all, framework#4019) or a 501 `NOT_IMPLEMENTED`
1374
- * (REST's dataset route with no service behind it). Degrading to a
1375
- * client-side aggregate over a scoped `find()` is CORRECT here.
1376
- * - **`rejected`** — the server refused OUR body (400 `VALIDATION_FAILED`;
1377
- * framework#4010 validates `/analytics/query` at the entry). Degrading
1378
- * would answer our own contract violation with plausible numbers from a
1379
- * different code path and bury it — the misdirection framework#3878
1380
- * documented. It must be surfaced.
1381
- *
1382
- * Anything else (5xx, network) is `unknown`: degrade, but silently — it is a
1383
- * transient failure, not a deployment that is missing a capability.
1219
+ * ADR-0112 / framework#3842 shape this repo already reads elsewhere).
1220
+ *
1221
+ * ## The `code` is the contract; the status is a transport fact (objectui#5721)
1222
+ *
1223
+ * What stood here tested `status === 404 || status === 501` BEFORE the code
1224
+ * operands that followed them, so the status short-circuited every one of
1225
+ * them: any 404 on this face was `not-installed` whatever code it carried, and
1226
+ * `NOT_IMPLEMENTED` / `ROUTE_NOT_FOUND` were unreachable for the very
1227
+ * conditions they name. Three unrelated conditions answer 404 on this url —
1228
+ *
1229
+ * route absent 404 `ROUTE_NOT_FOUND` (runtime dispatcher, framework#4019)
1230
+ * cube unknown 404 `CUBE_NOT_FOUND` (service-analytics' inference gate)
1231
+ * object unknown 404 `OBJECT_NOT_FOUND` (the `/data` fallback's own answer)
1232
+ *
1233
+ * — so a misspelled cube read as "this deployment has no analytics", told the
1234
+ * operator to install a server plugin, and re-answered the chart from a
1235
+ * different code path. Same defect as objectui#5663, arrived at from the other
1236
+ * direction: the mapping read a field it did not quote. Branch on the `code`;
1237
+ * consult the status only as a residual, when the answer declared no code at
1238
+ * all. Comparisons go through `errorCodeIs`/`errorCodeIsAnyOf` — the pre- and
1239
+ * post-ADR-0112 spellings both have to match (`@object-ui/types`).
1240
+ *
1241
+ * ### Both envelope families reach this function already decorated (MEASURED)
1242
+ *
1243
+ * `/analytics/query` exits through `@objectstack/runtime`'s
1244
+ * `dispatcher-plugin.errorResponseBase`, i.e. the **wrapped** `{ success:
1245
+ * false, error: { code, message } }` shape — not the flat one the dataset
1246
+ * route writes — and its 401 is `enforceAuth`'s **flat** `ANONYMOUS_DENY_BODY`.
1247
+ * The client's fetch wrapper flattens BOTH before throwing:
1248
+ * `errorBody?.code ?? errorBody?.error?.code` → `error.code`, plus
1249
+ * `error.httpStatus = res.status`. So no envelope reading belongs here; unlike
1250
+ * `queryDataset` (which owns its `fetch` and must read the body itself), this
1251
+ * function is handed the already-flattened error and reads one field.
1252
+ *
1253
+ * ## Each branch names ONE of the three outcomes the caller can take
1254
+ *
1255
+ * - **`not-installed`** — *degrade LOUDLY*. The deployment has no analytics
1256
+ * service: 501 `NOT_IMPLEMENTED` (route mounted, nothing behind it) or 404
1257
+ * `ROUTE_NOT_FOUND` (framework#4019 stops mounting the routes at all).
1258
+ * A client-side aggregate over a scoped `find()` answers the chart
1259
+ * correctly, and the operator is told once that the semantic layer is off.
1260
+ * - **`rejected`** — *THROW*. The server refused OUR body (400
1261
+ * `VALIDATION_FAILED`; framework#4010 validates `/analytics/query` at the
1262
+ * entry). Degrading would answer our own contract violation with plausible
1263
+ * numbers from a different code path and bury it — the misdirection
1264
+ * framework#3878 documented.
1265
+ * - **`unauthenticated`** — *THROW*. 401 `UNAUTHENTICATED`: the request was
1266
+ * refused before it ran, so it is evidence about the SESSION and none at
1267
+ * all about the capability. Degrading is not merely misleading here, it is
1268
+ * futile: the fallback's `find()` carries the same lapsed token and is
1269
+ * refused the same way, so the chart cannot be answered either. "Sign in
1270
+ * again" is an action; "install @objectstack/service-analytics" is not.
1271
+ * - **`cube-not-found`** — *THROW*. 404 `CUBE_NOT_FOUND`
1272
+ * (`analytics-service.assertInferableCube`, framework#3867): analytics IS
1273
+ * installed and answering — the NAME does not exist. That gate throws only
1274
+ * when the name is neither a registered cube nor a registered object, and
1275
+ * the fallback asks `/data/<the same name>`, which objectql's
1276
+ * `assertObjectRegistered` (framework#3770) answers 404 `OBJECT_NOT_FOUND`.
1277
+ * So degrading cannot produce numbers; it can only swap a message that
1278
+ * names the fix ("Define a Cube in your stack, or check the object name")
1279
+ * for a distant one, behind a warning that instructs the wrong repair.
1280
+ * - **`unknown`** — *degrade SILENTLY*. Anything else (5xx, network, a code
1281
+ * this consumer does not know): a transient failure, not a deployment
1282
+ * missing a capability and not a request that named something absent.
1384
1283
  */
1385
1284
  declare function classifyAnalyticsFailure(error: unknown): {
1386
- kind: 'not-installed' | 'rejected' | 'unknown';
1285
+ kind: 'not-installed' | 'rejected' | 'unauthenticated' | 'cube-not-found' | 'unknown';
1387
1286
  code?: string;
1388
1287
  message?: string;
1389
1288
  };
@@ -1434,6 +1333,50 @@ declare function isConcurrentUpdateError(error: unknown): error is ConcurrentUpd
1434
1333
  * instead of showing one undirected toast.
1435
1334
  */
1436
1335
  declare function normaliseClientError(error: unknown): unknown;
1336
+ /**
1337
+ * Fold an @objectstack/client HTTP-failure `meta` bag into the log MESSAGE.
1338
+ *
1339
+ * The client already hands us everything worth knowing —
1340
+ * `logger.error("HTTP request failed", undefined, { method, url, status, error })`
1341
+ * — but it hands it as the THIRD argument. Everything that flattens a console
1342
+ * record to text (a headless/CDP console capture, a log shipper, a copied
1343
+ * DevTools line) keeps only the message and renders the rest as `[object
1344
+ * Object]` / `Object`, so a wall of failures carried no method, no URL and no
1345
+ * status: the reporter of objectui#4042 had to diff the network panel by hand
1346
+ * to find out that 30 red lines were all one benign pre-login burst.
1347
+ *
1348
+ * So the identifying fields go into the string itself, and the structured bag
1349
+ * is STILL passed alongside for DevTools to expand — text for the flatteners,
1350
+ * object for the inspectors, neither at the other's expense.
1351
+ *
1352
+ * Exported for tests. Returns `null` when `meta` carries none of the three
1353
+ * fields, so callers keep the original message rather than printing a husk.
1354
+ */
1355
+ declare function formatHttpFailureMessage(message: string, meta?: Record<string, any>): string | null;
1356
+ /**
1357
+ * Build a Logger compatible with @objectstack/client that (a) spells every
1358
+ * request failure out in the message — see {@link formatHttpFailureMessage} —
1359
+ * and (b) demotes expected 404 noise to console.debug. The client logs every
1360
+ * non-2xx response with
1361
+ * `logger.error("HTTP request failed", undefined, { method, url, status, error })`,
1362
+ * but 404s on optional collections (sys_presence, sys_activity, …) are part of
1363
+ * normal degraded operation when those plugins aren't installed on the
1364
+ * server — they should not surface as errors in the browser DevTools.
1365
+ *
1366
+ * NOTE the asymmetry, and keep it: 404-on-an-optional-collection is demoted
1367
+ * because it is an EXPECTED outcome of a request we still mean to make. No
1368
+ * other status is demoted — a 401 that survives the console's session gate
1369
+ * (objectui#4042: a mid-session expiry, say) is a real event and must stay a
1370
+ * visible, fully-identified error. The cure for doomed requests is not issuing
1371
+ * them, never hiding them once issued.
1372
+ *
1373
+ * Returned object is loosely typed because the spec's Logger interface lives
1374
+ * in a transitive package; using `any` keeps us decoupled.
1375
+ *
1376
+ * Exported so the console's log contract is testable, and so an app wiring its
1377
+ * own `ObjectStackClient` gets the same identified failures.
1378
+ */
1379
+ declare function createQuietHttpLogger(): any;
1437
1380
  /**
1438
1381
  * Connection state for monitoring
1439
1382
  */
@@ -1480,6 +1423,115 @@ interface WriteWarningEvent {
1480
1423
  /** Event listener type for write-warning (dropped-fields) events. */
1481
1424
  type WriteWarningListener = (event: WriteWarningEvent) => void;
1482
1425
 
1426
+ /**
1427
+ * Resolve which object a `type='view'` metadata item belongs to.
1428
+ *
1429
+ * The metadata index is name-only, not field-typed: `GET /api/v1/meta/view`
1430
+ * accepts `?package=` and `?preview=draft` and nothing else (measured on
1431
+ * framework `packages/rest/src/rest-server.ts` — the `GET /meta/:type`
1432
+ * handler — and on `client.meta.getItems(type, { packageId })`). So every
1433
+ * reader of the view namespace enumerates `type='view'` once and narrows to
1434
+ * one object HERE, client-side.
1435
+ *
1436
+ * ONE spelling, one place, deliberately: {@link ObjectStackAdapter.listViews}
1437
+ * and {@link ObjectStackAdapter.listViewOverrides} read the same rows out of
1438
+ * the same namespace, and two private copies of "which object is this?" is a
1439
+ * drift waiting to happen — the switcher showing a view whose override the
1440
+ * grid cannot find, or the reverse.
1441
+ *
1442
+ * `object` is the identity field the write path stamps (and that the
1443
+ * framework's overlay heals onto identity-less personalization rows —
1444
+ * objectstack#2555); `data.object` is the config's data-provider target and
1445
+ * `objectName` the legacy artifact spelling.
1446
+ *
1447
+ * **Exported** since objectui#4373, for the same one-spelling reason: a writer
1448
+ * outside this module that holds a view BODY but not its object name (app-shell's
1449
+ * `MetadataService`) needs the object to name the keys
1450
+ * {@link ObjectStackAdapter.invalidateViewKeys} drops, and a fourth private copy
1451
+ * of "which object is this?" is exactly the drift this accessor exists to
1452
+ * prevent. Identity only — it names no cache key.
1453
+ */
1454
+ declare function viewItemObjectName(item: any): string | undefined;
1455
+ /**
1456
+ * The keys a personalization overlay row legitimately OWNS (objectui#5233).
1457
+ *
1458
+ * One per `persistViewPatch` call site in app-shell's `ObjectView` — the ONLY
1459
+ * production writer of these rows — read off the tree rather than recalled:
1460
+ * `rowHeight` (the density toggle, spec-canonical since #2890), `sort`,
1461
+ * `hiddenFields`, `columnState` and `inlineEdit`. Nothing else in such a row
1462
+ * is an opinion the user expressed; anything else it carries is a COPY of the
1463
+ * source view as it stood at write time, because `persistViewPatch` USED TO
1464
+ * send `{ ...baseViewDef, ...patch }` and this adapter persists what it is
1465
+ * given.
1466
+ *
1467
+ * That copy was the defect the maintainer ruled on (objectstack#7494, comment
1468
+ * 5261754173): an overlay written by a mere column drag froze the view's
1469
+ * effective `filter` — and its `columns`, `label`, `type`, `isDefault` … — as
1470
+ * of that moment, and because the display merge is `{ ...source, ...override }`
1471
+ * the frozen copy SHADOWED the source view forever. An admin then edited the
1472
+ * view's filter and every user who once resized a column kept the old one,
1473
+ * with nothing anywhere reporting it.
1474
+ *
1475
+ * Both halves of that ruling have now landed, and this adapter's behaviour is
1476
+ * unchanged by either — it still persists what it is given:
1477
+ *
1478
+ * - **read** (PR #5272, {@link narrowPersonalizationOverlay}): the consumer
1479
+ * that MERGES an overlay over a source view contributes only these keys, so
1480
+ * every already-stored fat row stops shadowing its source.
1481
+ * - **write** (objectui#5233, `buildPersistedViewBody` in app-shell's
1482
+ * `ObjectView`, unblocked by `columnState`'s admission to the view-metadata
1483
+ * surface as a runtime-only overlay key — objectstack#9933, released in
1484
+ * `@objectstack/spec` 17.1.0): a *system view's* overlay is now written as
1485
+ * the patch alone, so no new row freezes anything, and because the write is
1486
+ * a whole-document PUT the next toggle also strips an old fat row. A *saved
1487
+ * view's* own row is deliberately still written whole — for it the body IS
1488
+ * the view, not a copy of one.
1489
+ *
1490
+ * A fat row is therefore a legacy shape, not a shape this product still
1491
+ * produces; the list below is still what a reader is allowed to trust from one.
1492
+ *
1493
+ * ⛔ Do not grow this list to make some other key "stick" through an overlay.
1494
+ * A key that belongs to the view belongs in the view; the overlay is a patch,
1495
+ * and a patch that carries the whole document is what this list exists to
1496
+ * stop. Adding a sixth entry is only correct alongside a sixth
1497
+ * `persistViewPatch` call site — {@link narrowPersonalizationOverlay} is what
1498
+ * a reader checks that against.
1499
+ */
1500
+ declare const VIEW_OVERLAY_OWNED_KEYS: readonly ["rowHeight", "sort", "hiddenFields", "columnState", "inlineEdit"];
1501
+ /**
1502
+ * Reduce a personalization overlay row to the keys it owns — the read-side
1503
+ * half of objectui#5233, and the half that reaches rows ALREADY STORED.
1504
+ *
1505
+ * Rows written before this shipped carry the whole source view (see
1506
+ * {@link VIEW_OVERLAY_OWNED_KEYS}). Of the three dispositions the issue names
1507
+ * for them — strip on next write, migrate, tolerate on read — this is the
1508
+ * third, chosen deliberately and stated here rather than left implicit,
1509
+ * because it is the only one that is already true for every existing row the
1510
+ * moment it ships: strip-on-next-write heals a row only when its user happens
1511
+ * to touch that view again (and leaves the frozen filter live until then),
1512
+ * and a migration needs a runner this product does not have for `sys_metadata`
1513
+ * rows an operator may not even know exist. What the issue forbids is SILENT
1514
+ * tolerance; this is the explicit, pinned kind (`viewOverlayPatchOnly.test.ts`
1515
+ * in this package, `ObjectView.overlayPatchOnly.test.tsx` in app-shell).
1516
+ *
1517
+ * Applied by the consumer that MERGES an override over a source view
1518
+ * (app-shell's `sanitizeViewOverride`, the one seam both of
1519
+ * `loadViewOverrides`' read branches pass through), NOT by
1520
+ * {@link ObjectStackAdapter.listViewOverrides} / {@link ObjectStackAdapter.getView}:
1521
+ * those two answer with the stored DOCUMENT, their equality is itself pinned
1522
+ * ("same key space, same document" — `listViewOverrides.test.ts`), and
1523
+ * `InterfaceListPage` hydrates a hollow view out of that document. Narrowing
1524
+ * belongs where a row is read AS A PATCH, not where it is read as a row.
1525
+ *
1526
+ * Non-overlay rows — a genuine saved view's own body, which the same batch
1527
+ * read enumerates — are returned by REFERENCE, untouched: for those the row
1528
+ * IS the view, and every key on it is an opinion its author expressed.
1529
+ * Classification is {@link isPersonalizationOverlayRow}, the same predicate
1530
+ * {@link ObjectStackAdapter.listViews} excludes rows by, so a row cannot be a
1531
+ * saved view for one reader and an overlay for the other.
1532
+ */
1533
+ declare function narrowPersonalizationOverlay<T>(row: T): T;
1534
+
1483
1535
  /**
1484
1536
  * ObjectStack Data Source Adapter
1485
1537
  *
@@ -1532,6 +1584,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1532
1584
  private atomicBatchCapability;
1533
1585
  private mutationListeners;
1534
1586
  private writeWarningListeners;
1587
+ private saveAdvisoryListeners;
1535
1588
  constructor(config: {
1536
1589
  baseUrl: string;
1537
1590
  token?: string;
@@ -1652,6 +1705,82 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1652
1705
  * this to toast the user; the write itself already succeeded.
1653
1706
  */
1654
1707
  onWriteWarning(callback: WriteWarningListener): () => void;
1708
+ /**
1709
+ * Subscribe to metadata save-advisory events — the runtime authoring gate's
1710
+ * advisory findings on a save that SUCCEEDED (#4237; backend
1711
+ * objectstack#7435). Returns an unsubscribe function.
1712
+ *
1713
+ * Deliberately the same seam as {@link onWriteWarning} (#3431/#3455), which
1714
+ * is what {@link MetadataSaveAdvisoryEvent}'s own declaration already said it
1715
+ * was modelled on. It is a SIBLING of that channel rather than a second
1716
+ * payload pushed down it: `WriteWarningEvent` is a closed shape whose
1717
+ * `droppedFields` is required and means "fields the write legally stripped",
1718
+ * so carrying advisories on it would either force every existing
1719
+ * `onWriteWarning` consumer to grow a branch or make the event lie about what
1720
+ * happened. The seam's SHAPE is what is reused here — a long-lived instance
1721
+ * with a `subscribe → unsubscribe` registration that `AdapterProvider` wires
1722
+ * once — not its event type.
1723
+ *
1724
+ * Why here and not on the config, which is how the other client class does it
1725
+ * (#4133/#4236): `MetadataClient` is minted per component by
1726
+ * `useMetadataClient`, so it has no instance to subscribe to and its sink
1727
+ * rides the factory. `ObjectStackAdapter` is the opposite — one long-lived
1728
+ * instance per app, already carrying this exact subscription pattern.
1729
+ */
1730
+ onSaveAdvisory(callback: MetadataSaveAdvisoryListener): () => void;
1731
+ /**
1732
+ * Notify all save-advisory subscribers. Isolated exactly like
1733
+ * {@link emitWriteWarning}: a throwing listener must neither break the save
1734
+ * nor starve the others.
1735
+ */
1736
+ private emitSaveAdvisory;
1737
+ /**
1738
+ * Install the ONE emitter for the metadata save door (#4237).
1739
+ *
1740
+ * ## Why this seam, and what it covers
1741
+ *
1742
+ * `ObjectStackClient.meta.saveItem` is the second client class that writes
1743
+ * through `PUT /api/v1/meta/:type/:name`, and every one of its callers reaches
1744
+ * it through an adapter this class constructed — the four inside this file
1745
+ * (`updateViewConfig`, the two view paths, `updateDashboard`) via
1746
+ * `this.client`, and every caller outside it via {@link getClient}, which
1747
+ * hands back this same instance: `MetadataService` (app-shell, five saves),
1748
+ * `useNavigationSync`, and plugin-designer's Create/EditAppPage. Wrapping the
1749
+ * method once here therefore covers all of them WITHOUT a per-site edit, which
1750
+ * is the whole point — a toast copied into a dozen call sites is the shape
1751
+ * #4133 rejected for the other client class and it is no better here.
1752
+ *
1753
+ * `meta` is an own, writable property assigned per instance in the SDK's
1754
+ * constructor (`this.meta = { … }`), and the client this adapter builds is
1755
+ * never shared, so the wrap is bounded to an object this adapter owns for its
1756
+ * whole lifetime. It is not a prototype or global patch.
1757
+ *
1758
+ * ## Response shape — measured, not assumed
1759
+ *
1760
+ * The two client classes' envelopes coincide at the top level, which is what
1761
+ * makes `readSaveAdvisories` reusable unchanged across both. `SaveMetaItem-
1762
+ * ResponseSchema` puts `advisories` at the body's top level next to
1763
+ * `success` / `version` / `seq` / `state`, and the SDK's `unwrapResponse`
1764
+ * strips its `{ success, data }` envelope only when the body actually HAS a
1765
+ * `data` key — this body does not, so it is returned verbatim. So the same
1766
+ * reader that `MetadataClient.save` uses reads this response correctly, and
1767
+ * the pins in `onSaveAdvisory.test.ts` drive a real SDK client through a fake
1768
+ * `fetch` rather than stubbing `meta`, so that continues to be measured.
1769
+ *
1770
+ * ## Draft-door honesty (D1)
1771
+ *
1772
+ * Drafts are NEVER gated: the framework returns at its D1 early-return
1773
+ * (`if (args.state !== 'active') return null`) before running a rule, so a
1774
+ * draft save produces no findings to withhold. This client class has no draft
1775
+ * door at all to worry about — the SDK's `saveItem(type, name, item)` takes no
1776
+ * mode and always writes the active door, which is exactly why the gate DOES
1777
+ * run for its callers. `mode` on the emitted event is therefore derived from
1778
+ * the response's own `state` rather than from a request-side flag that does
1779
+ * not exist here: `'draft'` when the server says the row landed as a draft,
1780
+ * `'publish'` otherwise. That keeps the event truthful about which door it
1781
+ * came through instead of hard-coding one.
1782
+ */
1783
+ private installSaveAdvisoryInterceptor;
1655
1784
  create(resource: string, data: Partial<T>): Promise<T>;
1656
1785
  /**
1657
1786
  * Update an existing record.
@@ -1887,23 +2016,90 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1887
2016
  * @returns Promise resolving to discovery data, or null if not connected
1888
2017
  */
1889
2018
  getDiscovery(): Promise<unknown | null>;
2019
+ /**
2020
+ * **The one place that knows which cache keys a view-row write invalidates.**
2021
+ *
2022
+ * Two reads are cached under view-shaped keys, and both go stale when any
2023
+ * writer touches a view row for `objectName`:
2024
+ *
2025
+ * | reader | key |
2026
+ * |----------------------------|----------------------------|
2027
+ * | {@link getView} | `view:{object}:{viewName}` |
2028
+ * | {@link listViewOverrides} | `view-overrides:{object}` |
2029
+ *
2030
+ * Every writer routes here rather than restating that pair. The repo has now
2031
+ * paid twice for restatement: objectui#3778 removed five copies of a key
2032
+ * (`views:{object}`) that no reader had ever populated, and objectui#4363
2033
+ * fixed four copies that named only half the live set — and objectui#4373 is
2034
+ * the measured proof that the failure recurs by default, because the console's
2035
+ * real create/publish flow writes these same rows through the ADR-0034
2036
+ * metadata seam and never learned the key list at all. A pin suite can only
2037
+ * guard the writers that exist; a mandatory seam makes the next writer
2038
+ * structurally unable to forget.
2039
+ *
2040
+ * The rule is uniform per WRITE, not per branch: a write to a view row drops
2041
+ * both keys, in that order. Draft-addressed writes over-invalidate on purpose
2042
+ * — both readers enumerate PUBLISHED rows, so a draft write stales neither —
2043
+ * because the costs are not symmetric. An unnecessary invalidation costs one
2044
+ * refetch; a missed one costs up to the cache's 5-minute TTL of stale
2045
+ * overrides, and "which half am I in?" is not a question a future writer
2046
+ * should have to re-answer to stay correct.
2047
+ *
2048
+ * Public because the writers are not all in this class: app-shell's ADR-0034
2049
+ * seam (`runtime-metadata-persistence.ts`) and `MetadataService` write the
2050
+ * same rows through `client.meta` / the `/meta` draft-publish API, and they
2051
+ * reach this adapter as the `dataSource` they were already handed.
2052
+ *
2053
+ * @param objectName - Object the view belongs to (the `{object}` in both keys)
2054
+ * @param viewName - The view's canonical `name` — the identity
2055
+ * {@link getView} reads back by and {@link listViewOverrides} keys its map
2056
+ * with. Callers that hold a qualified `<object>.<key>` name pass it as-is:
2057
+ * it is the row key, not a path to be split.
2058
+ */
2059
+ invalidateViewKeys(objectName: string, viewName: string): void;
1890
2060
  /**
1891
2061
  * Batch-fetch all persisted view overrides for an object.
1892
2062
  *
1893
- * Per-view runtime overrides (density, column widths, sort, …) are
1894
- * stored in the metadata registry under key `<objectName>/<viewName>`.
1895
- * Loading them per-view fires N HTTP GETs that return 404 for views
1896
- * the user has never customized — generates console noise on every
1897
- * page load. This batch method performs a single
1898
- * `GET /api/v1/meta/<objectName>` (returns `{type, items}`) and
1899
- * returns a `{viewName: override}` map. Returns an empty map if the
1900
- * server doesn't expose the listing or returns no items.
2063
+ * Per-view runtime overrides (density, column widths, sort, hidden
2064
+ * columns, inlineEdit …) live in the SAME metadata namespace the
2065
+ * write path uses: `type='view'`, `name=<viewId>` (see
2066
+ * {@link updateViewConfig}). Loading them per-view fires N HTTP GETs
2067
+ * that 404 for every view the user never customized — console noise on
2068
+ * every page load. This batch method performs a single
2069
+ * `GET /api/v1/meta/view` (returns `{type, items}`) and narrows the
2070
+ * result to `objectName` client-side, exactly as {@link listViews}
2071
+ * does over the same rows (shared accessor: {@link viewItemObjectName}).
1901
2072
  *
1902
- * Result is cached identically to {@link getView}; saving a view via
1903
- * {@link updateViewConfig} invalidates the cache.
2073
+ * objectui#3774 — this used to enumerate `GET /api/v1/meta/<objectName>`,
2074
+ * putting the OBJECT name in the metadata TYPE slot. That key space is
2075
+ * disjoint from the one the write path lands in, so the batch map came
2076
+ * back empty for every object, forever, and every saved personalization
2077
+ * read back as "setting didn't save".
2078
+ *
2079
+ * FAILURES REJECT — they are not answered as `{}`. An empty map is an
2080
+ * authoritative "this object has no overrides" (callers may trust it and
2081
+ * skip the per-view reads); a transport/permission failure is "I could
2082
+ * not tell", and reporting the two as the same value is what made
2083
+ * ObjectView's per-view {@link getView} fallback unreachable code. When
2084
+ * we cannot tell, we do not pretend we can. Rejections are not cached
2085
+ * (the cache stores on success only), so a transient failure does not
2086
+ * pin an empty answer for the TTL.
2087
+ *
2088
+ * Result is cached identically to {@link getView}, and EVERY view write —
2089
+ * on this adapter and above it — invalidates it through the one seam,
2090
+ * {@link invalidateViewKeys}. For a long time only {@link updateViewConfig}
2091
+ * did (objectui#4363), which left the other three adapter paths stale
2092
+ * for the cache's 5-minute TTL. That gap does not self-heal: the consumer
2093
+ * (`loadViewOverrides`, app-shell `ObjectView`) treats a RESOLVED map as
2094
+ * authoritative and deliberately does not re-probe per view — objectui#3774,
2095
+ * and correct, since re-probing reinstates the 404 flurry the batch read
2096
+ * exists to remove. So a stale map here is served in full, and the per-view
2097
+ * {@link getView} fallback that would have masked it never runs.
1904
2098
  *
1905
2099
  * @param objectName - Object name (e.g. 'lead')
1906
2100
  * @returns Map keyed by view name with the persisted override config
2101
+ * @throws whatever the metadata transport throws — callers that have a
2102
+ * per-view fallback should catch and use it.
1907
2103
  */
1908
2104
  listViewOverrides(objectName: string): Promise<Record<string, any>>;
1909
2105
  /**
@@ -1918,23 +2114,54 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1918
2114
  */
1919
2115
  getView(objectName: string, viewId: string): Promise<unknown | null>;
1920
2116
  /**
1921
- * Persist a view definition for an object.
2117
+ * Persist a toolbar-driven view config patch — density, column widths,
2118
+ * sort, hidden columns, inline edit. Symmetric counterpart to
2119
+ * {@link getView}: writes the row to the server metadata store via
2120
+ * `client.meta.saveItem`, then invalidates the matching cache entry so the
2121
+ * next {@link getView} reflects the new payload. Returns the persisted item
2122
+ * when the server echoes it, otherwise undefined.
1922
2123
  *
1923
- * Symmetric counterpart to {@link getView}: writes the view to the
1924
- * server metadata store via `client.meta.saveItem`, then invalidates
1925
- * the matching cache entry so the next {@link getView} reflects the
1926
- * new payload. Returns the persisted item when the server echoes it,
1927
- * otherwise undefined.
2124
+ * Called from exactly ONE production site — `ObjectView`'s
2125
+ * `persistViewPatch`, for the toolbar toggle — but that ONE call site
2126
+ * fires for BOTH kinds of active tab: a code-defined **system** view (no
2127
+ * row of its own yet) and a genuinely user-created **saved** view (already
2128
+ * has a row — the toggle is editing ITS OWN definition, not laying an
2129
+ * overlay on top of it). Which one a given call means is NOT re-derived
2130
+ * here from the write's shape (objectui#4227's own lesson: shape inference
2131
+ * on this namespace is exactly what let a system view masquerade as
2132
+ * saved) — the caller already knows, via the same `isSavedViewId`
2133
+ * classification that gates the switcher's readonly flag and its five
2134
+ * mutating handlers, and passes it as {@link opts.isSavedView}.
1928
2135
  *
1929
- * Used by ObjectView for "live" toolbar persistence (density,
1930
- * column widths, sort, etc.) and by the View Config Panel for
1931
- * explicit saves.
2136
+ * - `isSavedView` false/omitted (system-view target, the common case and
2137
+ * the default for backward compatibility): stamps
2138
+ * {@link VIEW_OVERLAY_MARKER} so {@link listViews} excludes the row —
2139
+ * the original objectui#4227 fix.
2140
+ * - `isSavedView` true: the marker is withheld. Stamping it here would
2141
+ * flag the saved view's OWN row as a personalization overlay, and
2142
+ * `listViews()` would exclude it on the very next read — the user's own
2143
+ * view would vanish from the switcher the moment they toggled its
2144
+ * density (objectui#4227 follow-up, PM review on PR #4713, measured:
2145
+ * `persistViewPatch` has no gate on which kind of tab is active, and
2146
+ * this method writes to the exact same `(type='view', name=viewId)` key
2147
+ * {@link createView}/the ADR-0034 `viewEnvelope` seam already used for
2148
+ * that view, so the write is an upsert onto the saved view's row, not a
2149
+ * new one).
2150
+ *
2151
+ * Per objectstack#7494's ruling, the overlay this writes (system-view
2152
+ * case) is ORG-WIDE shared view settings, not a per-user preference — a
2153
+ * true per-user scope is a parked v18 direction on the platform side.
1932
2154
  *
1933
2155
  * @param objectName - Object name (e.g. 'lead')
1934
2156
  * @param viewId - View identifier (e.g. 'all_leads')
1935
2157
  * @param config - Full view definition to persist
2158
+ * @param opts.isSavedView - Whether `viewId` already names a saved view
2159
+ * (vs. a system view being personalized for the first time). Omit /
2160
+ * `false` for the default overlay-marking behavior.
1936
2161
  */
1937
- updateViewConfig(objectName: string, viewId: string, config: Record<string, any>): Promise<Record<string, any> | void>;
2162
+ updateViewConfig(objectName: string, viewId: string, config: Record<string, any>, opts?: {
2163
+ isSavedView?: boolean;
2164
+ }): Promise<Record<string, any> | void>;
1938
2165
  /**
1939
2166
  * List user-created views for a given object via the metadata overlay
1940
2167
  * API (ADR-0005). Replaces the legacy `find('sys_view', {...})` path
@@ -1942,8 +2169,10 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1942
2169
  * match the view spec shape.
1943
2170
  *
1944
2171
  * Returns view spec objects with their canonical `name` as identifier.
1945
- * Filters by `data.object === objectName` (or top-level `object`)
1946
- * client-side because the metadata index is name-only, not field-typed.
2172
+ * Narrows to one object client-side via {@link viewItemObjectName} —
2173
+ * the metadata index is name-only, not field-typed, so the route has no
2174
+ * `?object=` to push the filter down into. {@link listViewOverrides}
2175
+ * reads the same rows through the same accessor.
1947
2176
  */
1948
2177
  listViews(objectName: string, options?: {
1949
2178
  previewDrafts?: boolean;
@@ -1971,23 +2200,127 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1971
2200
  *
1972
2201
  * Generates a snake_case name if `spec.name` is not provided by appending
1973
2202
  * a short timestamp suffix to the source-name hint.
2203
+ *
2204
+ * Invalidates through {@link invalidateViewKeys}, like every other writer —
2205
+ * see {@link listViewOverrides} for why the batch map is the one that cannot
2206
+ * heal itself (objectui#4363).
1974
2207
  */
1975
2208
  createView(objectName: string, spec: Record<string, any>): Promise<Record<string, any> | void>;
1976
2209
  /**
1977
2210
  * Apply a partial update to an existing overlay view. Reads the current
1978
- * overlay (or seeds from artifact), merges, and writes back. ADR-0005
1979
- * overlay rows store the *full* view document, so partial updates require
1980
- * a read-merge-write cycle.
2211
+ * document, merges, and writes it back. ADR-0005 overlay rows store the
2212
+ * *full* view document, so partial updates require a read-merge-write cycle.
2213
+ *
2214
+ * **Both halves address the same row (#4139).** A view has two possible
2215
+ * homes and the read must resolve the one the write will target:
2216
+ *
2217
+ * - a pending per-item **draft** (`?state=draft` / `?mode=draft`) — where
2218
+ * ADR-0034 stages every runtime-created view, so a view made from the `+`
2219
+ * tab lives ONLY here until an explicit Publish;
2220
+ * - the **published** overlay (`client.meta.getItem` / `saveItem`).
2221
+ *
2222
+ * The draft is probed FIRST, and a hit is merged and written straight back
2223
+ * as a draft. Two things that ordering buys, both load-bearing:
2224
+ *
2225
+ * 1. A draft-only view is no longer invisible to the read. It used to 404,
2226
+ * and a `catch {}` labelled "treat missing as create-equivalent"
2227
+ * substituted `current = {}` — so a rename merged onto nothing and went
2228
+ * out as a `{label, name, object}` partial the server rejects (422),
2229
+ * while the draft row the UI reads back through `?preview=draft` kept the
2230
+ * old label. The edit was lost with no error surfaced to the user.
2231
+ * 2. A draft is never bypassed. Writing the published row while a draft is
2232
+ * pending would put the edit somewhere the draft shadows — and Publish
2233
+ * would then overwrite it with the pre-edit body, losing the change a
2234
+ * second time, later, where nothing connects it to this call.
2235
+ *
2236
+ * A draft edit stays a draft: `mode: 'draft'` keeps ADR-0037's guarantee
2237
+ * that nothing the preview shows goes live until Publish. Renaming a
2238
+ * *published* view (no draft pending) is unchanged — it writes the
2239
+ * published overlay, as before.
2240
+ *
2241
+ * **Both halves invalidate through {@link invalidateViewKeys}** (objectui#4363,
2242
+ * #4373) — same call, same key set, so the draft half's deliberate
2243
+ * over-invalidation is the seam's uniform rule rather than a per-branch
2244
+ * decision this method has to keep re-making.
2245
+ *
2246
+ * @throws when the view resolves in neither home, or when either read fails
2247
+ * for any other reason (network, permission). Both used to be swallowed
2248
+ * and converted into the bad partial write above; a caller that wants a
2249
+ * view created should call {@link createView}, which is the operation that
2250
+ * actually means "create".
1981
2251
  */
1982
2252
  updateView(objectName: string, viewName: string, partial: Record<string, any>): Promise<Record<string, any> | void>;
1983
2253
  /**
1984
- * Delete an overlay view (reset to artifact default if one exists, or
1985
- * remove entirely if it was a user-created view). Routes to
1986
- * `DELETE /api/v1/meta/view/:name`.
2254
+ * Delete an overlay view — from **every home it has**.
2255
+ *
2256
+ * A view has two possible homes, the same two {@link updateView} addresses:
2257
+ * the pending per-item **draft** (`DELETE /meta/view/:name?state=draft`) and
2258
+ * the **published** overlay (`DELETE /meta/view/:name`). This method used to
2259
+ * issue only the second, and the three cases came out like this (#4479):
2260
+ *
2261
+ * | case | before |
2262
+ * |--------------------|-----------------------------------------------------|
2263
+ * | draft-only view | BUG — the published delete answered `reset:false` / |
2264
+ * | | "nothing to delete", the draft survived, and the tab |
2265
+ * | | was still there after reload |
2266
+ * | published-only | correct — `reset:true`, tab gone |
2267
+ * | published + draft | ACCIDENTALLY correct — the published row went, so |
2268
+ * | | the tab went; the orphan draft stayed behind |
2269
+ *
2270
+ * **Why this is not the mechanical mirror of #4139.** `updateView` probes
2271
+ * the draft first and writes back to whichever home the read resolved, and
2272
+ * that is right for an update in all three cases. Copying it here would be
2273
+ * wrong in the third: `persistRuntimeMetadata` (app-shell) stages EVERY
2274
+ * runtime edit as a draft, so "publish a view, then edit it" routinely
2275
+ * produces a pair — and a draft-first-only delete on a pair discards the
2276
+ * draft and leaves the published row serving the view. That is not Delete
2277
+ * view, it is **Discard draft**, a deliberately different operation that
2278
+ * already exists (`discardRuntimeDraft`, documented as "the published
2279
+ * overlay is untouched"). The clean statement of the asymmetry: for an
2280
+ * update, one home is the right home; for a delete, "remove this view" is
2281
+ * satisfied only when NO home is left serving it.
2282
+ *
2283
+ * So both homes are deleted, **draft first**. The order is load-bearing on
2284
+ * the failure path: a fault between the two calls leaves the PUBLISHED
2285
+ * overlay intact, so the view is still served and the operation is cleanly
2286
+ * retryable. The reverse order would strand a draft-only view — which is
2287
+ * precisely the bug shape above.
2288
+ *
2289
+ * **Two blind calls, no probe.** The framework's `deleteMetaItem` answers a
2290
+ * missing home with a **200** carrying `reset:false` (`"No pending draft
2291
+ * for view/x."` / `"No view 'x' found — nothing to delete."`), never a 404,
2292
+ * so there is nothing for a probe to protect against. `updateView` needs its
2293
+ * probe for a different reason — its read must resolve the row the merge
2294
+ * writes back to — and that reason has no counterpart for a delete.
2295
+ *
2296
+ * **One transport, one error contract.** Both halves go through
2297
+ * {@link MetadataClient} (`reset`), the transport that can express the
2298
+ * `?state=` qualifier and the one `updateView`'s draft half already uses.
2299
+ * The published half used to go through `client.meta.deleteItem`; measured,
2300
+ * that issues the byte-identical request (`DELETE
2301
+ * {baseUrl}/api/v1/meta/view/:name`, no environment scoping is configured on
2302
+ * this adapter), so routing it here costs no addressing change and buys a
2303
+ * single `MetadataError` shape across both calls instead of two.
2304
+ *
2305
+ * @returns `deleted` is true only when no home is left serving the view AND
2306
+ * at least one actually held a row. A view that existed in neither home
2307
+ * still answers `false`, unchanged. The per-home outcomes are additive:
2308
+ * a partial result is observable rather than rounded up to `true`.
2309
+ * @throws when either delete fails, matching {@link updateView}'s
2310
+ * convention of surfacing the fault rather than degrading. A failure of
2311
+ * the PUBLISHED half after the draft was discarded carries the partial
2312
+ * state on the error's `outcome` — "draft gone, overlay left" is exactly
2313
+ * what the old `{ deleted: boolean }` could not express.
2314
+ *
2315
+ * Invalidates through {@link invalidateViewKeys}: the deleted row leaves the
2316
+ * batch override map too, and a ghost entry there is what the object page
2317
+ * would keep applying (objectui#4363). Fired in a `finally`, so it happens
2318
+ * once per call on EVERY outcome including the throw — after a half-failure
2319
+ * the draft row really is gone, and #4363's asymmetry decides it: an
2320
+ * unnecessary invalidation costs one refetch, a missed one costs the cache's
2321
+ * full 5-minute TTL of stale overrides.
1987
2322
  */
1988
- deleteView(objectName: string, viewName: string): Promise<{
1989
- deleted: boolean;
1990
- }>;
2323
+ deleteView(objectName: string, viewName: string): Promise<DeleteViewResult>;
1991
2324
  /**
1992
2325
  * Get an application definition by name or ID.
1993
2326
  * Attempts to fetch from the server metadata API.
@@ -1998,6 +2331,48 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1998
2331
  * @returns Promise resolving to the app definition or null
1999
2332
  */
2000
2333
  getApp(appId: string): Promise<unknown | null>;
2334
+ /**
2335
+ * Ask the by-name meta app route WHY an app is not in this session's app list
2336
+ * (objectui#4252).
2337
+ *
2338
+ * The app LIST is the generic metadata list route `GET /api/v1/meta/:type`,
2339
+ * requested with the singular type segment `app` — the same address this
2340
+ * method appends a name to below, and the one `MetadataProvider` reads its
2341
+ * items from. The server filters that list per session in `filterAppForUser`
2342
+ * (`packages/rest/src/rest-server.ts`, applied inside the `:type` list handler
2343
+ * once the type segment resolves to `app`), so an app withheld by
2344
+ * `requiredPermissions` and an app
2345
+ * that does not exist are byte-identical there: both are simply absent. A
2346
+ * console reading only that list has one fact and two conditions, and it
2347
+ * renders its copy for the wrong one — "it may still be publishing" over a
2348
+ * permanent authorization decision, which cost a downstream acceptance round
2349
+ * two test batches spent chasing a platform defect that was a missing
2350
+ * permission-set binding.
2351
+ *
2352
+ * The maintainer ruling (2026-08-12) put the answer on the BY-NAME route
2353
+ * rather than in the list, so the enumeration surface is not widened past what
2354
+ * a by-name probe already implies (objectstack#8013 / PR #8135): an app that
2355
+ * exists and whose `requiredPermissions` the session lacks answers `403` with
2356
+ * `PERMISSION_DENIED` in the declared envelope, and absence — a nonexistent
2357
+ * name, an unpublished app, an app gated by an absent optional service —
2358
+ * keeps answering `404 RESOURCE_NOT_FOUND`.
2359
+ *
2360
+ * ## Why this is a separate method and not a flavour of {@link getApp}
2361
+ *
2362
+ * - `getApp` degrades EVERY failure to `null`, which is exactly the
2363
+ * conflation this exists to undo; changing it would silently re-point its
2364
+ * own callers' fallback-to-static-config path.
2365
+ * - `getApp` memoises in `metadataCache`. A verdict about the CALLER must not
2366
+ * be cached beside a document about the APP — one grant, and a cached
2367
+ * denial outlives the session it described.
2368
+ *
2369
+ * Nothing here throws: a probe that cannot reach an answer returns `unknown`
2370
+ * and the caller keeps whatever it was already showing. Only the measured
2371
+ * `code` produces `denied` — never a status, never a message (objectui#4408).
2372
+ *
2373
+ * @param appName - the app name as it appears in the URL segment
2374
+ */
2375
+ probeAppAccess(appName: string): Promise<AppAccessVerdict>;
2001
2376
  /**
2002
2377
  * Get a page definition from ObjectStack.
2003
2378
  * Uses the metadata API to fetch page layouts.
@@ -2057,44 +2432,67 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2057
2432
  * silently returning wrong numbers.
2058
2433
  *
2059
2434
  * @param dataset - An inline dataset definition (draft) OR a saved dataset name.
2060
- * @param selection - Dimension/measure names to project + runtime directives.
2061
- */
2062
- queryDataset(dataset: Record<string, unknown> | string, selection: {
2063
- dimensions?: string[];
2064
- measures: string[];
2065
- runtimeFilter?: Record<string, unknown>;
2066
- timeDimensions?: unknown[];
2067
- compareTo?: {
2068
- kind: 'previousPeriod' | 'previousYear';
2069
- dimension: string;
2070
- };
2071
- order?: Record<string, 'asc' | 'desc'>;
2072
- limit?: number;
2073
- offset?: number;
2074
- timezone?: string;
2075
- /** Marginal-aggregate groupings (e.g. `[rows, [colDim], []]`) — server
2076
- * computes each subtotal with the measure's TRUE aggregate (never client
2077
- * re-derived). `[]` is the grand total. */
2078
- totals?: {
2079
- groupings: string[][];
2080
- };
2081
- }): Promise<{
2435
+ * @param selection - The spec's {@link DatasetSelection} — dimension/measure
2436
+ * names to project plus runtime directives. This parameter IS the spec type
2437
+ * by reference, never a local restatement of it (objectui#3613): a hand
2438
+ * copy of a contract is a second dialect of it, and the copy this replaced
2439
+ * had already drifted three ways from `@objectstack/spec` — it required
2440
+ * `compareTo.dimension` (optional since objectstack#5011, and resolved by
2441
+ * the EXECUTOR, so requiring it pushed callers into exactly the
2442
+ * consumer-side dimension guess AGENTS.md #0.1 forbids), it widened
2443
+ * `timeDimensions` to `unknown[]` and `runtimeFilter` to
2444
+ * `Record<string, unknown>`, and it had never grown `dateGranularity` at
2445
+ * all. Pinned in `queryDataset.test.ts`.
2446
+ */
2447
+ queryDataset(dataset: Record<string, unknown> | string, selection: DatasetSelection): Promise<{
2082
2448
  rows: Array<Record<string, unknown>>;
2083
- /** Column metadata: a display `label` (dimensions and measures), and a
2084
- * measure's numeral `format` + declared `currency` for value formatting. */
2085
- fields: Array<{
2086
- name: string;
2087
- type: string;
2088
- label?: string;
2089
- format?: string;
2090
- currency?: string;
2091
- }>;
2449
+ /**
2450
+ * Column metadata — the spec's `AnalyticsResult.fields[]` element BY
2451
+ * REFERENCE, never a local restatement of it (objectui#3752). Read
2452
+ * `@objectstack/spec` for what a column carries; this comment deliberately
2453
+ * does not re-list the keys, because the enumeration it replaced was the
2454
+ * bug: it named five (`name`/`type`/`label`/`format`/`currency`) and stopped
2455
+ * at the contract of the day it was written, so it never grew
2456
+ * `percentScale` — the server's answer to whether a percentage column is a
2457
+ * 0–1 fraction or already percentage points. The spec says a renderer that
2458
+ * receives it "must scale by it instead of guessing from the value"
2459
+ * (objectui#3136), so a declaration that hides the key steers a typed
2460
+ * consumer into exactly the guess-by-magnitude the issue banned. Pinned in
2461
+ * `queryDataset.test.ts`.
2462
+ *
2463
+ * Only this element is spec-owned: the envelope around it (`object` /
2464
+ * `dimensionFields` / `drillRawRows`) is ADR-0021 D2 drill metadata the REST
2465
+ * route adds on top of `AnalyticsResult`, and this method never returns the
2466
+ * result's `sql`, so the whole envelope is NOT an `AnalyticsResult`.
2467
+ */
2468
+ fields: Array<AnalyticsResult['fields'][number]>;
2092
2469
  /** ADR-0021 D2 drill-through: the dataset's base object (records to drill into). */
2093
2470
  object?: string;
2094
2471
  /** Drillable dimension NAME → underlying object FIELD name. */
2095
2472
  dimensionFields?: Record<string, string>;
2096
2473
  /** Raw grouped values per row (aligned to `rows` by index) for drill filters. */
2097
2474
  drillRawRows?: Array<Record<string, unknown>>;
2475
+ /**
2476
+ * Half-open date-range drill scope per row (framework#1752), aligned to
2477
+ * `rows` by index: dimension NAME → the field and `[gte, lt)` bounds of that
2478
+ * row's time bucket. The RANGE companion to `drillRawRows`, which handles
2479
+ * equality dims only — a `dateGranularity` dimension groups a SPAN of
2480
+ * records into one bucket, so the server excludes date dims from
2481
+ * `dimensionFields`/`drillRawRows` and sends this sidecar instead.
2482
+ *
2483
+ * The entry type is `@object-ui/core`'s `DatasetDrillRange` BY REFERENCE,
2484
+ * not a local restatement of it (objectui#3613/#3752 discipline): the same
2485
+ * declaration is what `buildDatasetDrillFilter` — the single consumer that
2486
+ * turns these bounds into an ObjectQL `{ $gte, $lt }` — accepts, and what
2487
+ * `DatasetWidget` / `DatasetReportRenderer` type their state with. Nothing
2488
+ * in `@objectstack/spec` owns this shape yet (the server's own
2489
+ * `AnalyticsResultWithDrill` is local to `service-analytics`), so the shared
2490
+ * in-repo interface is the one contract available; restating it here would
2491
+ * make a third dialect of it. Like `drillRawRows`, only the ARRAY is
2492
+ * validated below — the bounds are unvalidated payload, which is exactly why
2493
+ * `DatasetDrillRange` declares them `unknown`.
2494
+ */
2495
+ drillRanges?: Array<Record<string, DatasetDrillRange>>;
2098
2496
  /** Server-computed marginal aggregates, one entry per requested grouping. */
2099
2497
  totals?: Array<{
2100
2498
  dimensions: string[];
@@ -2211,4 +2609,4 @@ declare function createObjectStackAdapter<T = unknown>(config: {
2211
2609
  reconnectDelay?: number;
2212
2610
  }): DataSource<T>;
2213
2611
 
2214
- export { AnalyticsNotInstalledError, AnalyticsQueryRejectedError, type AuditEventType, type AuditLogConfig, type AuditLogEntry, AuthenticationError, type BatchProgressEvent, type BatchProgressListener, BulkOperationError, type CSPConfig, type CloudDeploymentConfig, type CloudHostingConfig, type CloudMarketplaceEntry, CloudOperations, 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 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, generateContractManifest, getSharedDiscovery, is404Error, isAnalyticsNotInstalledError, isConcurrentUpdateError, isErrorType, isMalformedFilterError, isObjectStackError, normaliseClientError, readTransactionalBatchCapability, serializeOrderBy, snapToGrid, validatePluginContract };
2612
+ export { API_ACCESS_DENIED_CODES, APP_PERMISSION_DENIED_CODE, AnalyticsDatasetNotFoundError, AnalyticsNotInstalledError, AnalyticsQueryRejectedError, AnalyticsUnauthenticatedError, type AppAccessVerdict, AuthenticationError, type BatchProgressEvent, type BatchProgressListener, BulkOperationError, ConcurrentUpdateError, ConnectionError, type ConnectionState, type ConnectionStateEvent, type ConnectionStateListener, DataApiValidationError, FILTER_OPERATOR_ALIASES, MalformedFilterError, type MetadataAuditEntry, type MetadataAuditResponse, type MetadataCacheStats, MetadataClient, type MetadataClientConfig, type MetadataClientSaveOptions, type MetadataDeleteOptions, type MetadataDiagnostics, type MetadataDiagnosticsEntry, type MetadataDiagnosticsOptions, type MetadataDiagnosticsSummary, type MetadataDraftHeader, type MetadataError, type MetadataGetOptions, type MetadataHistoryOptions, type MetadataLayered, type MetadataListOptions, type MetadataLockState, MetadataNotFoundError, type MetadataOverlayScope, type MetadataReference, type MetadataSaveAdvisoryEvent, type MetadataSaveAdvisoryListener, type MetadataValidationIssue, ObjectStackAdapter, ObjectStackError, type ObjectStackUserStateAdapterOptions, type UserDataAdapter, VIEW_OVERLAY_OWNED_KEYS, type WriteWarningEvent, type WriteWarningListener, classifyAnalyticsFailure, clearSharedDiscoveryCache, createErrorFromResponse, createObjectStackAdapter, createObjectStackUserStateAdapter, createQuietHttpLogger, formatHttpFailureMessage, getSharedDiscovery, is404Error, isAnalyticsNotInstalledError, isApiAccessDeniedError, isAppPermissionDeniedError, isConcurrentUpdateError, isErrorType, isMalformedFilterError, isObjectStackError, narrowPersonalizationOverlay, normaliseClientError, readSaveAdvisories, readTransactionalBatchCapability, serializeOrderBy, viewItemObjectName };