@objectstack/metadata 17.1.0 → 17.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ import { MetadataFormat, MetadataLoaderContract, MetadataLoadOptions, MetadataLo
3
3
  export { MetadataCollectionInfo, MetadataDiffResult, MetadataFormat, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataManagerConfig, MetadataSaveOptions, MetadataSaveResult, MetadataStats, MetadataWatchEvent } from '@objectstack/spec/system';
4
4
  import { IMetadataService, IDataDriver, IDataEngine, IRealtimeService, MetadataWriteOptions, MetadataWatchCallback, MetadataWatchHandle, MetadataExportOptions, MetadataImportOptions, MetadataImportResult, MetadataTypeInfo, ApiEndpointMatch, IPubSub, ISchemaDriver } from '@objectstack/spec/contracts';
5
5
  export { IMetadataService, MetadataExportOptions, MetadataImportOptions, MetadataImportResult, MetadataTypeInfo, MetadataWatchCallback, MetadataWatchHandle } from '@objectstack/spec/contracts';
6
- import { MetadataTypeRegistryEntryParsed, MetadataQuery, MetadataQueryResult, MetadataBulkResult, MetadataOverlay, MetadataValidationResult, MetadataDependency, MetadataPluginConfig } from '@objectstack/spec/kernel';
6
+ import { MetadataTypeRegistryEntryParsed, MetadataQuery, MetadataQueryResult, MetadataBulkResult, MetadataValidationResult, MetadataDependency, MetadataPluginConfig } from '@objectstack/spec/kernel';
7
7
  export { MetadataBulkResult, MetadataDependency, MetadataPluginConfig, MetadataPluginManifest, MetadataQuery, MetadataQueryResult, MetadataType, MetadataTypeRegistryEntry, MetadataValidationResult } from '@objectstack/spec/kernel';
8
8
  import { Logger, Plugin, PluginContext } from '@objectstack/core';
9
9
  import { z } from 'zod';
@@ -72,6 +72,29 @@ interface MetadataSerializer {
72
72
  * Defines the contract for loading metadata from various sources
73
73
  */
74
74
 
75
+ /**
76
+ * [#14205] One loaded item paired with the KEY its store holds it under.
77
+ *
78
+ * The pair exists because a metadata body is not required to name itself. Most
79
+ * do — and for those the key and `data.name` agree, because
80
+ * `assertMetadataRegisterContract` refuses a `register(type, name, data)` whose
81
+ * `data.name` disagrees with the `name` argument. But an aggregated `defineView`
82
+ * container has no own `name` BY DESIGN (its identity is the target object), and
83
+ * `register()` explicitly allows that: "A document with NO `name` of its own is
84
+ * fine — the argument is the key".
85
+ *
86
+ * So the key is a fact about the STORE, not about the body, and it is the only
87
+ * identity a nameless item has. Carrying it BESIDE `data` rather than folding it
88
+ * into `data` is the whole point: the body stays byte-identical to what was
89
+ * stored, so no consumer sees a synthesised `name` and the register contract's
90
+ * `data.name` check keeps meaning what it means.
91
+ */
92
+ interface MetadataKeyedItem<T = any> {
93
+ /** The key this item is stored under — `register()`'s `name` argument. */
94
+ readonly name: string;
95
+ /** The stored body, exactly as {@link MetadataLoader.loadMany} would return it. */
96
+ readonly data: T;
97
+ }
75
98
  /**
76
99
  * Abstract interface for metadata loaders
77
100
  * Implementations can load from filesystem, HTTP, S3, databases, etc.
@@ -96,6 +119,34 @@ interface MetadataLoader {
96
119
  * @returns Array of loaded items
97
120
  */
98
121
  loadMany<T = any>(type: string, options?: MetadataLoadOptions): Promise<T[]>;
122
+ /**
123
+ * Load multiple items of a type, each paired with the KEY this loader holds
124
+ * it under.
125
+ *
126
+ * [#14205] Optional, and the reason it is a second method rather than a
127
+ * widened `loadMany()`: `MetadataLoader` is exported from this package's
128
+ * public entry, with implementors outside it (`packages/objectql`'s
129
+ * conformance fixtures among them). Changing `loadMany()`'s return type would
130
+ * break every one of them; an optional member breaks none, and a loader that
131
+ * cannot produce keys — `RemoteLoader`, whose wire format carries bodies only
132
+ * — simply does not declare it.
133
+ *
134
+ * `MetadataManager` prefers this method wherever it merges a loader's answer
135
+ * into a keyed set (`list()`, and the endpoint index), and falls back to
136
+ * `loadMany()` keyed by `data.name` when it is absent. That fallback is
137
+ * exactly the pre-#14205 behaviour, so it drops items whose body has no
138
+ * top-level `name`: implement this method on any loader that can be asked to
139
+ * hold one.
140
+ *
141
+ * `data` MUST be the same body `loadMany()` would return for the item —
142
+ * unmodified, in particular with no `name` folded in. `name` is the store's
143
+ * key, carried beside the body, never written into it.
144
+ *
145
+ * @param type The metadata type
146
+ * @param options Load options with patterns
147
+ * @returns Array of (key, body) pairs
148
+ */
149
+ loadManyKeyed?<T = any>(type: string, options?: MetadataLoadOptions): Promise<MetadataKeyedItem<T>[]>;
99
150
  /**
100
151
  * Check if item exists
101
152
  * @param type The metadata type
@@ -224,7 +275,6 @@ declare class MetadataManager implements IMetadataService {
224
275
  protected watchCallbacks: Map<string, Set<WatchCallback>>;
225
276
  protected config: MetadataManagerOptions;
226
277
  private registry;
227
- private overlays;
228
278
  private typeRegistry;
229
279
  private dependencies;
230
280
  private listCache;
@@ -534,6 +584,50 @@ declare class MetadataManager implements IMetadataService {
534
584
  * result may be memoized depends on what happened to the read's registration
535
585
  * while it ran, which only `list()` can see.
536
586
  */
587
+ /**
588
+ * Merge one loader's answer for `type` into `items`, under the identity that
589
+ * loader holds each item by.
590
+ *
591
+ * ## [#14205] The identity of a loader-held item is its ROW KEY
592
+ *
593
+ * Both plural readers used to key a loader's items by `body.name`, and admit
594
+ * an item only when the body carried a string one:
595
+ *
596
+ * ```ts
597
+ * if (itemAny && typeof itemAny.name === 'string' && !items.has(itemAny.name))
598
+ * ```
599
+ *
600
+ * A body is not required to name itself. `register(type, name, data)` takes
601
+ * the key as its ARGUMENT, and `assertMetadataRegisterContract` says in as
602
+ * many words that "A document with NO `name` of its own is fine — the argument
603
+ * is the key". An aggregated `defineView` container is exactly that: no own
604
+ * `name` by design, identity carried in the row's `name` column.
605
+ *
606
+ * So the old gate dropped every such item the moment the registry went cold
607
+ * and only the loader could answer — a persisted view container vanished from
608
+ * `list('view')` after a restart, and `listDiagnosed()` called the short
609
+ * answer complete because no loader had thrown. Same gate, same effect, in
610
+ * `listForIndex()`: a nameless `api` row fell out of the endpoint index, where
611
+ * a miss reads as "nothing declares this route".
612
+ *
613
+ * The repair is to ask the loader for the key instead of guessing it from the
614
+ * body ({@link MetadataLoader.loadManyKeyed}), and to keep the key BESIDE the
615
+ * body: nothing is written into a body that deliberately has none, so the
616
+ * register contract's refusal of a disagreeing `data.name` still means what it
617
+ * says.
618
+ *
619
+ * Nothing consumers see today changes shape. For any item that went through
620
+ * `register()`, a `data.name` that exists is required to EQUAL the key, so the
621
+ * keyed merge produces the identical map entry; what is new is only the
622
+ * entries the old gate refused. The `loadMany()` fallback below is the
623
+ * pre-#14205 behaviour verbatim, for loaders that cannot produce keys
624
+ * (`RemoteLoader`'s wire format carries bodies only).
625
+ *
626
+ * Read failures are NOT caught here: `readListUncached` warns-and-continues,
627
+ * `listForIndex` deliberately throws, and that difference is each caller's to
628
+ * keep.
629
+ */
630
+ private admitLoaderItems;
537
631
  private readListUncached;
538
632
  /**
539
633
  * Report — at `error`, once per outage episode — that a loader could not be
@@ -803,6 +897,35 @@ declare class MetadataManager implements IMetadataService {
803
897
  * Runtime-authored `shared` / `personal` views (`sys_view_definition`) are
804
898
  * merged in by the REST layer; this method returns the `package` layer that
805
899
  * was registered from source.
900
+ *
901
+ * ## [#13913] Aggregated containers are expanded inline, per read
902
+ *
903
+ * `this.list('view')` is `MetadataManager`'s OWN loader-based store — the
904
+ * in-memory registry plus every registered loader — and is a completely
905
+ * different store from the `sys_metadata` rows `getMetaItems` reads. #13407
906
+ * taught `getMetaItems` to expand a runtime-authored aggregated container
907
+ * inline; this exit never called it and had no equivalent step, so a
908
+ * container that `GET /meta/view?object=` now serves still answered **empty**
909
+ * here.
910
+ *
911
+ * Merely getting the container into the store would not have helped: the
912
+ * filter also requires `viewKind`, and a container has none. Loosening that
913
+ * requirement is NOT the repair — it would answer with the container itself
914
+ * as a view, the behaviour #7163 ruled wrong — so what is added below is the
915
+ * container's **expansion**, whose items each carry the `viewKind` + `object`
916
+ * pair this filter has always tested. The filter itself is untouched: it
917
+ * reads the top-level `object`, exactly as `ViewSchema.object` declares.
918
+ *
919
+ * Registry-free and per-read, mirroring #13407's choice at the other exit and
920
+ * for the same reason — the registry is process-wide, so a read must not
921
+ * graft rows into it (see `view-container-expansion.ts`'s header, which also
922
+ * records why the protocol's copy of this logic cannot be imported).
923
+ *
924
+ * Already-present items win: an expansion contributes only names the store
925
+ * does not already hold, so a container whose expanded ViewItems were
926
+ * registered by a source registrar (the ObjectQL boot loop, the artifact/HMR
927
+ * loader) still answers with those registered, fully-enriched items and this
928
+ * step adds nothing.
806
929
  */
807
930
  getViewsByObject(object: string): Promise<unknown[]>;
808
931
  /**
@@ -931,29 +1054,6 @@ declare class MetadataManager implements IMetadataService {
931
1054
  type: string;
932
1055
  name: string;
933
1056
  }>, options?: MetadataWriteOptions): Promise<MetadataBulkResult>;
934
- private overlayKey;
935
- /**
936
- * Get the active overlay for a metadata item
937
- */
938
- getOverlay(type: string, name: string, scope?: 'platform' | 'user'): Promise<MetadataOverlay | undefined>;
939
- /**
940
- * Save/update an overlay for a metadata item
941
- */
942
- saveOverlay(overlay: MetadataOverlay): Promise<void>;
943
- /**
944
- * Remove an overlay, reverting to the base definition
945
- */
946
- removeOverlay(type: string, name: string, scope?: 'platform' | 'user'): Promise<void>;
947
- /**
948
- * Get the effective (merged) metadata after applying all overlays.
949
- * Resolution order: system ← merge(platform) ← merge(user)
950
- */
951
- getEffective(type: string, name: string, context?: {
952
- userId?: string;
953
- tenantId?: string;
954
- roles?: string[];
955
- permissions?: string[];
956
- }): Promise<unknown | undefined>;
957
1057
  /**
958
1058
  * Watch for metadata changes (IMetadataService contract).
959
1059
  * Returns a handle for unsubscribing.
@@ -1294,7 +1394,7 @@ interface MetadataPluginOptions {
1294
1394
  }
1295
1395
  declare class MetadataPlugin implements Plugin {
1296
1396
  name: string;
1297
- type: string;
1397
+ type: "standard";
1298
1398
  version: string;
1299
1399
  /**
1300
1400
  * Services init() UNCONDITIONALLY registers (ADR-0116, #4131) — lets the
@@ -1315,6 +1415,13 @@ declare class MetadataPlugin implements Plugin {
1315
1415
  private repository?;
1316
1416
  /** Chokidar watcher on the artifact file (local-file mode) — ADR-0008 PR-8. */
1317
1417
  private artifactWatcher?;
1418
+ /**
1419
+ * The context handed to `init()`, retained so `destroy()` can log.
1420
+ * [#10772] `Plugin.destroy()` takes NO argument — it is the kernel's only
1421
+ * teardown hook — so the context the old `stop(ctx)` alias received has to
1422
+ * be captured at init time instead of arriving at teardown time.
1423
+ */
1424
+ private initCtx?;
1318
1425
  /**
1319
1426
  * The most recently parsed artifact metadata (the plural-field record:
1320
1427
  * `objects`, `views`, `data`, …). Carried on the `metadata:reloaded`
@@ -1323,14 +1430,117 @@ declare class MetadataPlugin implements Plugin {
1323
1430
  * `name` and are skipped by `_parseAndRegisterArtifact`'s register loop.
1324
1431
  */
1325
1432
  private lastParsedMetadata?;
1433
+ /**
1434
+ * Once-per-process dedupe for the summaries the versioned artifact window
1435
+ * emits. The artifact watcher replays `_parseAndRegisterArtifact` on every
1436
+ * file change, so without this a dev loop over a legacy artifact would
1437
+ * re-announce the same finding on every reload — the same shape
1438
+ * `Protocol.storedConversionWarned` guards on the stored-row pass, which
1439
+ * this surfacing is modeled on.
1440
+ *
1441
+ * Two key families share the set, because they share the replay:
1442
+ * `<conversionId>|<label>` for a forward-conversion summary (#12772), and
1443
+ * `unbound-form-predicate-root|<label>` for the unbound-root notice
1444
+ * (#12915) — one line per artifact there, not one per conversion, since
1445
+ * the notice already aggregates every finding it made.
1446
+ */
1447
+ private artifactConversionWarned;
1326
1448
  constructor(options?: MetadataPluginOptions);
1327
1449
  init: (ctx: PluginContext) => Promise<void>;
1328
1450
  start: (ctx: PluginContext) => Promise<void>;
1329
- stop: (ctx: PluginContext) => Promise<void>;
1451
+ /**
1452
+ * Teardown — the kernel's ONLY teardown hook.
1453
+ *
1454
+ * [#10772] This body used to be spelled `stop(ctx)`. `Plugin`
1455
+ * (`@objectstack/core`'s `types.ts`) declares `init()`, `start?(ctx)` and
1456
+ * `destroy?()` and no `stop()`, and `ObjectKernel.performShutdown()` /
1457
+ * `LiteKernel.destroy()` walk the plugins in reverse calling
1458
+ * `plugin.destroy()` — so the artifact watcher, the manager and the
1459
+ * repository were all still held after `await kernel.shutdown()` had
1460
+ * RESOLVED. The `start`/`stop` pair read symmetric to a reviewer because
1461
+ * `start()` really is on the interface; only one half was ever called.
1462
+ *
1463
+ * Idempotent: every handle is cleared as it is released, so a second
1464
+ * teardown is a no-op rather than a second close.
1465
+ */
1466
+ destroy: () => Promise<void>;
1467
+ /**
1468
+ * Retained alias for {@link destroy}. Kept because it is public API of an
1469
+ * exported class: an embedder may have learned to call it directly
1470
+ * precisely BECAUSE the kernel never did, and deleting it would break them.
1471
+ * Still an arrow property, so a detached `const { stop } = plugin` call
1472
+ * keeps working too. The parameter is now optional and ignored —
1473
+ * `destroy()` takes no context, so teardown logs through the context
1474
+ * captured in `init()`.
1475
+ */
1476
+ stop: (_ctx?: PluginContext) => Promise<void>;
1330
1477
  /**
1331
1478
  * Fetch JSON content from a URL with configurable timeout.
1332
1479
  */
1333
1480
  private _fetchJson;
1481
+ /**
1482
+ * Versioned ADR-0087 forward conversion at the artifact-ingestion door
1483
+ * (#12772) — runs BEFORE the strict schema parse below, because the parse
1484
+ * is the refusal point.
1485
+ *
1486
+ * A compiled artifact is data at rest with a version stamp: built by
1487
+ * released tooling, then unchanged while the platform moves on. When a
1488
+ * spec release retires an authorable key inside a protocol line (spec
1489
+ * 17.1 → 17.2 retired the `allowRestore`/`allowPurge` permission bits),
1490
+ * every already-built artifact carrying the key becomes unbootable at the
1491
+ * tombstone — with no operator remedy, since `os migrate meta` targets
1492
+ * sources, not built artifacts. The stored-row read path already replays
1493
+ * the conversion chain for exactly this reason
1494
+ * (`applyConversionsToStoredItem`, ADR-0087 addendum); this is the same
1495
+ * policy at the artifact door, **keyed off the artifact's own declared
1496
+ * `engines.protocol` floor**: an artifact authored below the running spec
1497
+ * version converts forward, an artifact authored at the current (or a
1498
+ * newer) surface converts nothing and answers to the strict parse,
1499
+ * tombstones included. The version key is what keeps this a conversion
1500
+ * rather than an amnesty — the retired keys return with the M2 lifecycle
1501
+ * batch (#1883), and artifacts authored against that surface must never
1502
+ * have them stripped by history.
1503
+ *
1504
+ * Notices surface the way the stored-row pass's do — operator-visible and
1505
+ * deduped — as one summary line per conversion per artifact rather than
1506
+ * one per rewritten path (a real 17.1 artifact carried 150 strips of the
1507
+ * same two keys; 150 identical warn lines would bury the boot log).
1508
+ */
1509
+ private _convertArtifactForward;
1510
+ /**
1511
+ * Operator-facing boot notice for form-view predicates that fault OPEN on
1512
+ * this runtime (#12915 scope C — maintainer ruling 2026-08-28, 「同意C」).
1513
+ *
1514
+ * A form-view predicate binds `record` / `previous` / `parent` (runtime
1515
+ * record forms) or `data` (metadata-editing forms) — and a FIELD-level one
1516
+ * also binds `current_user` and its ADR-0068 aliases (objectui#6010),
1517
+ * which a SECTION-level one does not. The contract states beside that
1518
+ * vocabulary that a bare identifier is UNBOUND and the predicate faults,
1519
+ * and `visibleWhen`'s fault fallback is `true`. On a real
1520
+ * 17.1-built artifact that combination dead-ends record creation in the
1521
+ * console: the conditionally hidden field renders, and its unconditional
1522
+ * `required: true` — authored to be gated by the visibility that no longer
1523
+ * applies — blocks every submit, while the same payload POSTs 201 through
1524
+ * REST. Nothing refused, nothing logged, and only the operator can fix it
1525
+ * (by rebuilding the artifact), so this is the channel the ruling picked:
1526
+ * service startup, server-side, never a console surface — the person at
1527
+ * the form cannot act on "your artifact is stale".
1528
+ *
1529
+ * **Detection only.** No refusal, no rewrite, no behaviour change: the
1530
+ * predicate keeps faulting open exactly as before. Rewriting a bare root to
1531
+ * `record.` is the ADR-0087 conversion (#12915 scope A), deferred by the
1532
+ * same ruling with an explicit start line.
1533
+ *
1534
+ * **Same versioned window as the conversion replay above** — and read off
1535
+ * that pass's own verdict rather than recomputed, so the two can never
1536
+ * disagree about which artifacts are "old". An artifact declaring the
1537
+ * current (or a newer) floor answers to the strict parse and gets nothing
1538
+ * from here even when it does carry bare roots; that boundary is what keeps
1539
+ * a notice about legacy artifacts out of contract territory. An undeclared
1540
+ * range is treated as old data at rest, matching the grandfathering posture
1541
+ * the window already takes (`converted-undeclared`).
1542
+ */
1543
+ private _warnUnboundFormPredicateRoots;
1334
1544
  /**
1335
1545
  * Parse raw artifact JSON (envelope or bare definition) and register all
1336
1546
  * metadata items into the MetadataManager.
@@ -1344,6 +1554,35 @@ declare class MetadataPlugin implements Plugin {
1344
1554
  * landing.
1345
1555
  */
1346
1556
  private _parseAndRegisterArtifact;
1557
+ /**
1558
+ * Register ONE artifact body's collections into the MetadataManager.
1559
+ *
1560
+ * A "body" is either the whole artifact (the single-package branch, where
1561
+ * the artifact and its one package are the same object) or one entry of
1562
+ * `packages[]` (ADR-0130 D4), which is an assembled
1563
+ * `{ ...manifest, ...collections }` payload carrying the same collection
1564
+ * keys the top level does. The loop is identical for both — that is the
1565
+ * point: there is one ingestion of a collection here, not one per shape.
1566
+ *
1567
+ * @param provenance - The `(packageId, packageVersion)` every item found in
1568
+ * this body is stamped with (ADR-0010 §3.7, via `applyProtection`). It is
1569
+ * the body's OWN identity, never the enclosing artifact's, which is what
1570
+ * makes a multi-package artifact's items agree with the registry and with
1571
+ * `GET /api/v1/packages` about who owns them.
1572
+ * @param slots.claim - Called with every `(type, name)` this pass
1573
+ * registered. Passed when reading package bodies; the residual sweep uses
1574
+ * what it recorded.
1575
+ * @param slots.skip - Consulted before registering each `(type, name)`.
1576
+ * Passed ONLY by the residual sweep, so a package body's copy is never
1577
+ * overwritten by the flattened top-level copy of the same definition —
1578
+ * the overwrite that re-attributed the item to the artifact's manifest.
1579
+ * ⛔ It is never passed while reading the bodies themselves: two items of
1580
+ * one name inside one body still register as they always have (last
1581
+ * wins), because suppressing that would be a behaviour change on the
1582
+ * single-package branch D7 pins.
1583
+ * @returns How many items this body registered.
1584
+ */
1585
+ private _registerArtifactBodyCollections;
1347
1586
  /**
1348
1587
  * Reload the artifact from disk into the MetadataManager, then announce a
1349
1588
  * generic `metadata:reloaded` hook. Used by BOTH reload paths (the HMR POST
@@ -1386,6 +1625,16 @@ declare class MemoryLoader implements MetadataLoader {
1386
1625
  private storage;
1387
1626
  load(type: string, name: string, _options?: MetadataLoadOptions): Promise<MetadataLoadResult>;
1388
1627
  loadMany<T = any>(type: string, _options?: MetadataLoadOptions): Promise<T[]>;
1628
+ /**
1629
+ * [#14205] The keyed half of {@link loadMany}. The storage map is already
1630
+ * `Type -> Name -> Data`, so the key this loader holds an item under is the
1631
+ * map key — `loadMany()` was simply discarding it, which dropped every
1632
+ * nameless body out of `MetadataManager.list()` and out of the endpoint index.
1633
+ *
1634
+ * The body is handed back by reference, unchanged: the key travels beside it,
1635
+ * never folded into it.
1636
+ */
1637
+ loadManyKeyed<T = any>(type: string, _options?: MetadataLoadOptions): Promise<MetadataKeyedItem<T>[]>;
1389
1638
  exists(type: string, name: string): Promise<boolean>;
1390
1639
  stat(type: string, name: string): Promise<MetadataStats | null>;
1391
1640
  list(type: string): Promise<string[]>;
@@ -1718,7 +1967,33 @@ declare class DatabaseLoader implements MetadataLoader {
1718
1967
  */
1719
1968
  private rethrowUnlessTableUnprovisioned;
1720
1969
  load(type: string, name: string, _options?: MetadataLoadOptions): Promise<MetadataLoadResult>;
1970
+ /**
1971
+ * The one type-wide read both plural readers share: every row of `type`, each
1972
+ * body paired with the `name` COLUMN it was stored under.
1973
+ *
1974
+ * [#14205] `name` is `null` only for a row whose key column does not hold a
1975
+ * string. Such a row is still a body {@link loadMany} must return — dropping
1976
+ * it would change what consumers see today — but it has no usable identity,
1977
+ * so {@link loadManyKeyed} filters it out rather than invent one.
1978
+ *
1979
+ * One query and one cache entry serve both methods: `loadMany()` used to own
1980
+ * them, and splitting them would have made every keyed `list()` read miss the
1981
+ * cache and re-hit the database.
1982
+ */
1983
+ private readTypeRows;
1721
1984
  loadMany<T = any>(type: string, _options?: MetadataLoadOptions): Promise<T[]>;
1985
+ /**
1986
+ * [#14205] The keyed half of {@link loadMany} — see
1987
+ * {@link MetadataKeyedItem} for why the row key travels beside the body
1988
+ * instead of inside it.
1989
+ *
1990
+ * `DatabaseLoader` is where the defect was measured: an aggregated view
1991
+ * container is written by `register('view', OBJECT, container)` and stored
1992
+ * verbatim, so its `sys_metadata` row carries the identity in the `name`
1993
+ * COLUMN and the body has none. {@link rowToData} returns that body without
1994
+ * folding the column in — deliberately, and unchanged here.
1995
+ */
1996
+ loadManyKeyed<T = any>(type: string, _options?: MetadataLoadOptions): Promise<MetadataKeyedItem<T>[]>;
1722
1997
  exists(type: string, name: string): Promise<boolean>;
1723
1998
  stat(type: string, name: string): Promise<MetadataStats | null>;
1724
1999
  list(type: string): Promise<string[]>;
@@ -1903,4 +2178,61 @@ declare class TypeScriptSerializer implements MetadataSerializer {
1903
2178
  getFormat(): MetadataFormat;
1904
2179
  }
1905
2180
 
1906
- export { DatabaseLoader, type DatabaseLoaderOptions, HistoryCleanupManager, JSONSerializer, MemoryLoader, type MetadataLoader, MetadataManager, type MetadataManagerOptions, MetadataPlugin, type MetadataSerializer, index as Migration, RemoteLoader, type SerializeOptions, TypeScriptSerializer, type WatchCallback, YAMLSerializer, calculateChecksum, generateDiffSummary, generateSimpleDiff };
2181
+ /**
2182
+ * [#13913] Registry-free expansion of an aggregated `defineView` container
2183
+ * that reached `MetadataManager`'s OWN backing store.
2184
+ *
2185
+ * ---------------------------------------------------------------------------
2186
+ * Why this lives in `packages/metadata` and does NOT import the protocol's copy
2187
+ * ---------------------------------------------------------------------------
2188
+ * `packages/metadata-protocol` grew `expandRuntimeViewContainer` (#13407) for
2189
+ * `getMetaItems`' own inline expansion, and the obvious move would be to share
2190
+ * it. **The dependency edge forbids it**: `@objectstack/metadata-protocol`
2191
+ * declares `@objectstack/metadata` as a dependency, so an import in this
2192
+ * direction inverts an existing edge and closes a cycle. Promoting the
2193
+ * protocol's private method to a public export would not help either — it
2194
+ * would widen that package's surface *and* still be unreachable from here.
2195
+ *
2196
+ * The reusable substance is therefore taken from where it already is: the
2197
+ * canonical expansion primitives (`isAggregatedViewContainer`,
2198
+ * `expandViewContainer`) live in `@objectstack/spec`, one level BELOW both
2199
+ * packages, and this package already imports them (`plugin.ts`). Nothing is
2200
+ * duplicated except the ~6-line object-derivation chain — which is exactly the
2201
+ * part that has silently drifted three ways (the ObjectQL boot loop keys off
2202
+ * the registration name, `plugin.ts` walks two levels, and `protocol.ts` walks
2203
+ * four since #13407). {@link deriveViewContainerObject} is the one spelling of
2204
+ * it for this package, so the drift has a single place to be repaired rather
2205
+ * than a third private copy to fall behind.
2206
+ *
2207
+ * ---------------------------------------------------------------------------
2208
+ * Registry-free, on purpose
2209
+ * ---------------------------------------------------------------------------
2210
+ * Nothing here REGISTERS. `protocol.ts`'s own header records why its
2211
+ * registry-MUTATING path (`hydrateExpandedViewItems`) could not simply be
2212
+ * widened — it is gated off for every org-scoped row (ADR-0005: the registry
2213
+ * is shared by every org a kernel serves) — and why #13407's actual repair was
2214
+ * a separate, registry-free, per-request expansion. The same reasoning applies
2215
+ * one exit over: `MetadataManager`'s registry is process-wide too, so the
2216
+ * repair for {@link MetadataManager.getViewsByObject} is a pure function over
2217
+ * what that one read already holds.
2218
+ */
2219
+
2220
+ /**
2221
+ * Which object an aggregated view container binds to.
2222
+ *
2223
+ * The container's OWN top-level `object` field — `ViewSchema.object`,
2224
+ * documented there as "how a stack-level `views: [...]` entry says which object
2225
+ * its views belong to; read by `getViewsByObject()` / `GET /meta/view?object=`"
2226
+ * — is the authorial, explicit signal and is consulted FIRST (#13407). The
2227
+ * three-deep fallback below it is kept unchanged for every container written
2228
+ * before that field was read here: `list.data.object`, then `form.data.object`,
2229
+ * then the row's own `name` — which is the bound object only by convention, and
2230
+ * is why a container that set the top-level field but not `list.data.object`
2231
+ * used to bind under the wrong key or not at all.
2232
+ *
2233
+ * Returns `undefined` when no binding can be derived; every caller treats that
2234
+ * as "no expansion" rather than an error.
2235
+ */
2236
+ declare function deriveViewContainerObject(container: unknown): string | undefined;
2237
+
2238
+ export { DatabaseLoader, type DatabaseLoaderOptions, HistoryCleanupManager, JSONSerializer, MemoryLoader, type MetadataKeyedItem, type MetadataLoader, MetadataManager, type MetadataManagerOptions, MetadataPlugin, type MetadataSerializer, index as Migration, RemoteLoader, type SerializeOptions, TypeScriptSerializer, type WatchCallback, YAMLSerializer, calculateChecksum, deriveViewContainerObject, generateDiffSummary, generateSimpleDiff };