@objectstack/metadata 17.2.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.cjs CHANGED
@@ -38,9 +38,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
38
38
  // src/routes/hmr-routes.ts
39
39
  var hmr_routes_exports = {};
40
40
  __export(hmr_routes_exports, {
41
+ isDevMetadataEndpointEnabled: () => isDevMetadataEndpointEnabled,
41
42
  registerMetadataHmrRoutes: () => registerMetadataHmrRoutes
42
43
  });
44
+ function isDevMetadataEndpointEnabled(env = process.env) {
45
+ return (env.NODE_ENV ?? "").trim().toLowerCase() === "development";
46
+ }
43
47
  function registerMetadataHmrRoutes(app, manager, options = {}) {
48
+ if (!isDevMetadataEndpointEnabled()) return null;
44
49
  const routePath = options.path ?? "/api/v1/dev/metadata-events";
45
50
  const listeners = /* @__PURE__ */ new Set();
46
51
  const broadcast = (evt) => {
@@ -200,11 +205,12 @@ __export(index_exports, {
200
205
  MetadataPlugin: () => MetadataPlugin,
201
206
  Migration: () => migration_exports,
202
207
  RemoteLoader: () => RemoteLoader,
203
- SysMetadataHistoryObject: () => import_metadata_core3.SysMetadataHistoryObject,
204
- SysMetadataObject: () => import_metadata_core3.SysMetadataObject,
208
+ SysMetadataHistoryObject: () => import_metadata_core4.SysMetadataHistoryObject,
209
+ SysMetadataObject: () => import_metadata_core4.SysMetadataObject,
205
210
  TypeScriptSerializer: () => TypeScriptSerializer,
206
211
  YAMLSerializer: () => YAMLSerializer,
207
212
  calculateChecksum: () => calculateChecksum,
213
+ deriveViewContainerObject: () => deriveViewContainerObject,
208
214
  generateDiffSummary: () => generateDiffSummary,
209
215
  generateSimpleDiff: () => generateSimpleDiff
210
216
  });
@@ -384,7 +390,7 @@ export default metadata;
384
390
  };
385
391
 
386
392
  // src/loaders/database-loader.ts
387
- var import_metadata_core = require("@objectstack/metadata-core");
393
+ var import_metadata_core2 = require("@objectstack/metadata-core");
388
394
  var import_spec = require("@objectstack/spec");
389
395
  var import_shared = require("@objectstack/spec/shared");
390
396
 
@@ -552,141 +558,49 @@ var LRUCache = class {
552
558
  }
553
559
  };
554
560
 
555
- // src/utils/schema-sync-errors.ts
561
+ // src/loaders/database-loader.ts
556
562
  var import_types = require("@objectstack/types");
557
- var ALREADY_EXISTS = {
558
- codes: /* @__PURE__ */ new Set([
559
- // PostgreSQL SQLSTATE (class 42 — syntax error or access rule violation)
560
- "42P07",
561
- // duplicate_table
562
- "42701",
563
- // duplicate_column
564
- "42710",
565
- // duplicate_object — index / constraint already exists
566
- // MySQL / MariaDB (mysql2 puts the symbolic name on `code`)
567
- "ER_TABLE_EXISTS_ERROR",
568
- // 1050
569
- "ER_DUP_FIELDNAME",
570
- // 1060
571
- "ER_DUP_KEYNAME"
572
- // 1061
573
- ]),
574
- errnos: /* @__PURE__ */ new Set([1050, 1060, 1061]),
575
- /**
576
- * Message fallback for drivers that carry no machine-readable code —
577
- * notably SQLite, whose `code` is the undifferentiated `SQLITE_ERROR` for
578
- * every DDL failure, so the message is the only signal available:
579
- * - `table sys_metadata already exists`
580
- * - `duplicate column name: environment_id`
581
- * - `index idx_x already exists`
582
- * Postgres phrases its own as `relation "x" already exists` /
583
- * `column "x" of relation "y" already exists`, which matches the same test.
584
- */
585
- message: /already exists|duplicate column name|duplicate key name/i
586
- };
587
- var MISSING_TABLE = {
588
- codes: /* @__PURE__ */ new Set([
589
- "42P01",
590
- // PostgreSQL undefined_table
591
- "ER_NO_SUCH_TABLE"
592
- // MySQL / MariaDB 1146
593
- ]),
594
- errnos: /* @__PURE__ */ new Set([1146]),
595
- /**
596
- * - SQLite / libsql: `no such table: sys_metadata_history`
597
- * - PostgreSQL: `relation "sys_metadata_history" does not exist`
598
- * - MySQL/MariaDB: `Table 'app.sys_metadata_history' doesn't exist`
599
- */
600
- message: /no such table|relation ["'`][^"'`]+["'`] does not exist|table ["'`][^"'`]+["'`] doesn'?t exist|unknown table/i,
601
- excludes: {
602
- /**
603
- * Exactly the three SQLSTATEs the docblock above already names as
604
- * must-stay-loud neighbours of `does not exist`. They are listed here
605
- * rather than merely trusted to miss the message test, because two of
606
- * them (42703 columns, 42704 constraints/triggers) have a phrasing that
607
- * *does* hit it, and because a code is a fact where prose is a guess.
608
- *
609
- * Postgres-shaped on purpose: measured, neither MySQL
610
- * (`Unknown column 'label' in 'field list'`) nor SQLite
611
- * (`no such column: bogus`, `table t has no column named label`)
612
- * phrases a sub-object failure so that a missing-table phrase falls out
613
- * of it, so there is nothing there to exclude. Adding their codes would
614
- * be surface with no defect behind it.
615
- */
616
- codes: /* @__PURE__ */ new Set([
617
- "42703",
618
- // undefined_column
619
- "42704",
620
- // undefined_object — constraint, trigger, role, type, …
621
- "3D000"
622
- // invalid_catalog_name — `database "x" does not exist`
623
- ]),
624
- /**
625
- * `«sub-object» "x" of relation "y" …` — Postgres' phrasing for a
626
- * failure about something *inside* a relation, which therefore says the
627
- * relation itself is present. The two in-repo siblings that carry this
628
- * phrase are `mapDataError` (`packages/rest`, #5352) and
629
- * `service-analytics`'s missing-column subtraction (#6035/PR #6346).
630
- *
631
- * [#6615] All three now read one home — `@objectstack/types` — instead
632
- * of three hand-kept copies, so the phrase can no longer be taught to
633
- * the repo a fourth time or drift in one package only. The **width**
634
- * difference that used to justify the copy is preserved and is the
635
- * reason the home exports two functions rather than one: those two
636
- * *extract* the column name to phrase a better error, so a miss costs a
637
- * vaguer message; this one *excludes*, so a miss restores the
638
- * corruption. {@link isRelationSubObjectPhrase} is therefore the wider
639
- * question — it drops their `column`/`[a-z0-9_]+`/`does not exist`
640
- * anchors: any sub-object, any quoted identifier, any verdict.
641
- * Over-matching here only ever converts a benign verdict into a loud
642
- * one, which is the direction this whole module already errs in.
643
- */
644
- matchesMessage: import_types.isRelationSubObjectPhrase
563
+
564
+ // src/migrations/driver-exec.ts
565
+ function resolveDriverExec(driver) {
566
+ const candidate = driver;
567
+ if (!candidate) return void 0;
568
+ if (typeof candidate.execute === "function") {
569
+ return (sql, bindings) => candidate.execute(sql, bindings ? [...bindings] : []);
645
570
  }
646
- };
647
- var MAX_CAUSE_DEPTH = 4;
648
- function matchesDriverError(error, signature, depth) {
649
- if (error === null || error === void 0 || depth > MAX_CAUSE_DEPTH) return false;
650
- if (typeof error === "string") {
651
- if (signature.excludes?.matchesMessage(error)) return false;
652
- return signature.message.test(error);
653
- }
654
- if (typeof error !== "object") return false;
655
- const err = error;
656
- const excludes = signature.excludes;
657
- if (excludes) {
658
- if (typeof err.code === "string" && excludes.codes.has(err.code)) return false;
659
- if (typeof err.message === "string" && excludes.matchesMessage(err.message)) return false;
660
- }
661
- if (typeof err.code === "string" && signature.codes.has(err.code)) return true;
662
- if (typeof err.errno === "number" && signature.errnos.has(err.errno)) return true;
663
- if (typeof err.message === "string" && signature.message.test(err.message)) return true;
664
- return matchesDriverError(err.cause, signature, depth + 1);
665
- }
666
- function isSchemaAlreadyExistsError(error, depth = 0) {
667
- return matchesDriverError(error, ALREADY_EXISTS, depth);
571
+ if (typeof candidate.raw === "function") {
572
+ return (sql, bindings) => candidate.raw(sql, bindings ? [...bindings] : []);
573
+ }
574
+ return void 0;
668
575
  }
669
- function isMissingTableError(error, depth = 0) {
670
- return matchesDriverError(error, MISSING_TABLE, depth);
576
+ function driverExecRefusal(helper) {
577
+ return `${helper}: driver must expose an .execute(sql, bindings?) or .raw(sql, bindings?) method. SqlDriver (better-sqlite3/knex) exposes .execute(), as does its SqliteWasmDriver subclass; cloud-side TursoDriver also conforms.`;
671
578
  }
672
579
 
673
580
  // src/migrations/migrate-project-id-to-environment-id.ts
674
- var AFFECTED_TABLES = [
675
- "sys_metadata",
676
- "sys_metadata_history"
677
- ];
581
+ var import_metadata_core = require("@objectstack/metadata-core");
582
+ var SOURCE_COLUMN = "project_id";
583
+ var TARGET_COLUMN = "environment_id";
584
+ var CANDIDATE_OBJECTS = [import_metadata_core.SysMetadataObject, import_metadata_core.SysMetadataHistoryObject];
585
+ function declaresColumn(object, column) {
586
+ return Object.prototype.hasOwnProperty.call(object.fields ?? {}, column);
587
+ }
588
+ var CANDIDATE_TABLES = CANDIDATE_OBJECTS.map((o) => o.name);
589
+ var AFFECTED_TABLES = CANDIDATE_OBJECTS.filter((o) => declaresColumn(o, TARGET_COLUMN)).map((o) => o.name);
678
590
  async function migrateProjectIdToEnvironmentId(driver) {
679
- const driverAny = driver;
680
- if (typeof driverAny.raw !== "function") {
681
- throw new Error(
682
- "migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. SqlDriver (better-sqlite3/knex) supports this; cloud-side TursoDriver also conforms."
683
- );
591
+ const exec = resolveDriverExec(driver);
592
+ if (!exec) {
593
+ throw new Error(driverExecRefusal("migrateProjectIdToEnvironmentId"));
684
594
  }
685
595
  const results = [];
686
- for (const table of AFFECTED_TABLES) {
596
+ for (const table of CANDIDATE_TABLES) {
597
+ if (!AFFECTED_TABLES.includes(table)) {
598
+ results.push({ table, status: "skipped_not_declared" });
599
+ continue;
600
+ }
687
601
  try {
688
- const hasColumn = await _columnExists(driverAny, table, "project_id");
689
- const alreadyMigrated = await _columnExists(driverAny, table, "environment_id");
602
+ const hasColumn = await _columnExists(exec, table, SOURCE_COLUMN);
603
+ const alreadyMigrated = await _columnExists(exec, table, TARGET_COLUMN);
690
604
  if (alreadyMigrated && !hasColumn) {
691
605
  results.push({ table, status: "already_done" });
692
606
  continue;
@@ -695,8 +609,8 @@ async function migrateProjectIdToEnvironmentId(driver) {
695
609
  results.push({ table, status: "table_missing" });
696
610
  continue;
697
611
  }
698
- await driverAny.raw(
699
- `ALTER TABLE "${table}" RENAME COLUMN project_id TO environment_id`
612
+ await exec(
613
+ `ALTER TABLE "${table}" RENAME COLUMN ${SOURCE_COLUMN} TO ${TARGET_COLUMN}`
700
614
  );
701
615
  results.push({ table, status: "renamed" });
702
616
  } catch (err) {
@@ -705,14 +619,14 @@ async function migrateProjectIdToEnvironmentId(driver) {
705
619
  }
706
620
  return results;
707
621
  }
708
- async function _columnExists(driver, table, column) {
622
+ async function _columnExists(exec, table, column) {
709
623
  try {
710
- const rows = await driver.raw(`PRAGMA table_info("${table}")`);
624
+ const rows = await exec(`PRAGMA table_info("${table}")`);
711
625
  if (Array.isArray(rows) && rows.length > 0) {
712
626
  const list2 = Array.isArray(rows[0]) ? rows[0] : rows;
713
627
  return list2.some((r) => r?.name === column);
714
628
  }
715
- const result = await driver.raw(
629
+ const result = await exec(
716
630
  `SELECT column_name FROM information_schema.columns WHERE table_name = ? AND column_name = ?`,
717
631
  [table, column]
718
632
  );
@@ -724,6 +638,16 @@ async function _columnExists(driver, table, column) {
724
638
  }
725
639
 
726
640
  // src/loaders/database-loader.ts
641
+ function canonicalIsoInstant(value) {
642
+ if (value === null || value === void 0) return void 0;
643
+ if (value instanceof Date) return value.toISOString();
644
+ if (typeof value === "string") return value;
645
+ return String(value);
646
+ }
647
+ function isoFromValidDate(value) {
648
+ if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString();
649
+ return value;
650
+ }
727
651
  var DatabaseLoader = class {
728
652
  constructor(options) {
729
653
  this.contract = {
@@ -860,9 +784,11 @@ var DatabaseLoader = class {
860
784
  }
861
785
  return this.driver.create(table, data);
862
786
  }
787
+ // `null` is the driver path's not-found answer (`IDataDriver.update()`,
788
+ // #13878); both callers here resolve the row first and discard the result.
863
789
  async _update(table, id, data) {
864
790
  if (this.engine) {
865
- return this.engine.update(table, { id, ...data });
791
+ return this.engine.update(table, { ...data, id });
866
792
  }
867
793
  return this.driver.update(table, id, data);
868
794
  }
@@ -907,7 +833,7 @@ var DatabaseLoader = class {
907
833
  }
908
834
  return max + 1;
909
835
  } catch (error) {
910
- if (isMissingTableError(error)) return 1;
836
+ if ((0, import_types.isMissingTableError)(error, this.historyTableName)) return 1;
911
837
  throw error;
912
838
  }
913
839
  }
@@ -945,11 +871,11 @@ var DatabaseLoader = class {
945
871
  }
946
872
  try {
947
873
  await this.driver.syncSchema(this.tableName, {
948
- ...import_metadata_core.SysMetadataObject,
874
+ ...import_metadata_core2.SysMetadataObject,
949
875
  name: this.tableName
950
876
  });
951
877
  } catch (error) {
952
- if (!isSchemaAlreadyExistsError(error)) {
878
+ if (!(0, import_types.isSchemaAlreadyExistsError)(error)) {
953
879
  if (!this.schemaFailureReported) {
954
880
  this.schemaFailureReported = true;
955
881
  console.error(
@@ -984,7 +910,7 @@ var DatabaseLoader = class {
984
910
  }
985
911
  try {
986
912
  await this.driver.syncSchema(this.historyTableName, {
987
- ...import_metadata_core.SysMetadataHistoryObject,
913
+ ...import_metadata_core2.SysMetadataHistoryObject,
988
914
  name: this.historyTableName
989
915
  });
990
916
  if (this.historySchemaFailureReported) {
@@ -995,7 +921,7 @@ var DatabaseLoader = class {
995
921
  }
996
922
  this.historySchemaReady = true;
997
923
  } catch (error) {
998
- if (isSchemaAlreadyExistsError(error)) {
924
+ if ((0, import_types.isSchemaAlreadyExistsError)(error)) {
999
925
  this.historySchemaReady = true;
1000
926
  return;
1001
927
  }
@@ -1153,9 +1079,9 @@ var DatabaseLoader = class {
1153
1079
  source: row.source,
1154
1080
  tags: row.tags ? typeof row.tags === "string" ? JSON.parse(row.tags) : row.tags : void 0,
1155
1081
  createdBy: row.created_by,
1156
- createdAt: row.created_at,
1082
+ createdAt: isoFromValidDate(row.created_at),
1157
1083
  updatedBy: row.updated_by,
1158
- updatedAt: row.updated_at
1084
+ updatedAt: isoFromValidDate(row.updated_at)
1159
1085
  };
1160
1086
  }
1161
1087
  // ==========================================
@@ -1206,7 +1132,7 @@ var DatabaseLoader = class {
1206
1132
  * with its empty value.
1207
1133
  */
1208
1134
  rethrowUnlessTableUnprovisioned(error) {
1209
- if (isMissingTableError(error)) return;
1135
+ if ((0, import_types.isMissingTableError)(error, this.tableName)) return;
1210
1136
  throw error;
1211
1137
  }
1212
1138
  // ==========================================
@@ -1256,17 +1182,38 @@ var DatabaseLoader = class {
1256
1182
  };
1257
1183
  }
1258
1184
  }
1259
- async loadMany(type, _options) {
1185
+ /**
1186
+ * The one type-wide read both plural readers share: every row of `type`, each
1187
+ * body paired with the `name` COLUMN it was stored under.
1188
+ *
1189
+ * [#14205] `name` is `null` only for a row whose key column does not hold a
1190
+ * string. Such a row is still a body {@link loadMany} must return — dropping
1191
+ * it would change what consumers see today — but it has no usable identity,
1192
+ * so {@link loadManyKeyed} filters it out rather than invent one.
1193
+ *
1194
+ * One query and one cache entry serve both methods: `loadMany()` used to own
1195
+ * them, and splitting them would have made every keyed `list()` read miss the
1196
+ * cache and re-hit the database.
1197
+ */
1198
+ async readTypeRows(type) {
1260
1199
  await this.ensureSchema();
1261
1200
  if (this.loadManyCache) {
1262
1201
  const cached = this.loadManyCache.get(type);
1263
- if (cached !== void 0) return cached;
1202
+ if (cached !== void 0) {
1203
+ return cached;
1204
+ }
1264
1205
  }
1265
1206
  try {
1266
1207
  const rows = await this._find(this.tableName, {
1267
1208
  where: this.baseFilter(type)
1268
1209
  });
1269
- const result = rows.map((row) => this.rowToData(row)).filter((data) => data !== null);
1210
+ const result = [];
1211
+ for (const row of rows) {
1212
+ const data = this.rowToData(row);
1213
+ if (data === null) continue;
1214
+ const name = row.name;
1215
+ result.push({ name: typeof name === "string" && name !== "" ? name : null, data });
1216
+ }
1270
1217
  this.loadManyCache?.set(type, result);
1271
1218
  return result;
1272
1219
  } catch (error) {
@@ -1274,6 +1221,29 @@ var DatabaseLoader = class {
1274
1221
  return [];
1275
1222
  }
1276
1223
  }
1224
+ async loadMany(type, _options) {
1225
+ return (await this.readTypeRows(type)).map((entry) => entry.data);
1226
+ }
1227
+ /**
1228
+ * [#14205] The keyed half of {@link loadMany} — see
1229
+ * {@link MetadataKeyedItem} for why the row key travels beside the body
1230
+ * instead of inside it.
1231
+ *
1232
+ * `DatabaseLoader` is where the defect was measured: an aggregated view
1233
+ * container is written by `register('view', OBJECT, container)` and stored
1234
+ * verbatim, so its `sys_metadata` row carries the identity in the `name`
1235
+ * COLUMN and the body has none. {@link rowToData} returns that body without
1236
+ * folding the column in — deliberately, and unchanged here.
1237
+ */
1238
+ async loadManyKeyed(type, _options) {
1239
+ const entries = await this.readTypeRows(type);
1240
+ const keyed = [];
1241
+ for (const entry of entries) {
1242
+ if (entry.name === null) continue;
1243
+ keyed.push({ name: entry.name, data: entry.data });
1244
+ }
1245
+ return keyed;
1246
+ }
1277
1247
  async exists(type, name) {
1278
1248
  await this.ensureSchema();
1279
1249
  if (this.loadCache) {
@@ -1309,7 +1279,7 @@ var DatabaseLoader = class {
1309
1279
  const metadataStr = typeof row.metadata === "string" ? row.metadata : JSON.stringify(row.metadata);
1310
1280
  const stats = {
1311
1281
  size: metadataStr.length,
1312
- mtime: record.updatedAt ?? record.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1282
+ mtime: canonicalIsoInstant(record.updatedAt ?? record.createdAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
1313
1283
  format: "json",
1314
1284
  etag: record.checksum
1315
1285
  };
@@ -1370,7 +1340,7 @@ var DatabaseLoader = class {
1370
1340
  changeNote: row.change_note,
1371
1341
  organizationId: row.organization_id,
1372
1342
  recordedBy: row.recorded_by,
1373
- recordedAt: row.recorded_at
1343
+ recordedAt: isoFromValidDate(row.recorded_at)
1374
1344
  };
1375
1345
  }
1376
1346
  /**
@@ -1427,7 +1397,7 @@ var DatabaseLoader = class {
1427
1397
  changeNote: row.change_note,
1428
1398
  organizationId: row.organization_id,
1429
1399
  recordedBy: row.recorded_by,
1430
- recordedAt: row.recorded_at
1400
+ recordedAt: isoFromValidDate(row.recorded_at)
1431
1401
  };
1432
1402
  });
1433
1403
  return { records: result, total, hasMore };
@@ -1723,6 +1693,32 @@ var EndpointMatcher = class {
1723
1693
  }
1724
1694
  };
1725
1695
 
1696
+ // src/view-container-expansion.ts
1697
+ var import_spec2 = require("@objectstack/spec");
1698
+ var import_shared2 = require("@objectstack/spec/shared");
1699
+ function deriveViewContainerObject(container) {
1700
+ if (!container || typeof container !== "object") return void 0;
1701
+ const c = container;
1702
+ const own = typeof c.object === "string" && c.object ? c.object : void 0;
1703
+ const byName = typeof c.name === "string" && c.name ? c.name : void 0;
1704
+ return own ?? c?.list?.data?.object ?? c?.form?.data?.object ?? byName;
1705
+ }
1706
+ function expandRuntimeViewContainer(data) {
1707
+ if (!(0, import_spec2.isAggregatedViewContainer)(data)) return [];
1708
+ const container = data;
1709
+ const viewObject = deriveViewContainerObject(container);
1710
+ if (!viewObject) return [];
1711
+ const out = [];
1712
+ for (const vi of (0, import_spec2.expandViewContainer)(viewObject, container)) {
1713
+ (0, import_shared2.applyProtection)(vi, {
1714
+ packageId: container._packageId,
1715
+ packageVersion: container._packageVersion
1716
+ });
1717
+ out.push(vi);
1718
+ }
1719
+ return out;
1720
+ }
1721
+
1726
1722
  // src/metadata-manager.ts
1727
1723
  var WRITABLE_LOADER_METHODS = ["save", "delete"];
1728
1724
  var WRITABLE_LOADER_METHOD_SIGNATURE = {
@@ -1762,8 +1758,6 @@ var _MetadataManager = class _MetadataManager {
1762
1758
  this.watchCallbacks = /* @__PURE__ */ new Map();
1763
1759
  // In-memory metadata registry: type -> name -> data
1764
1760
  this.registry = /* @__PURE__ */ new Map();
1765
- // Overlay storage: "type:name:scope" -> MetadataOverlay
1766
- this.overlays = /* @__PURE__ */ new Map();
1767
1761
  // Type registry for metadata type info
1768
1762
  this.typeRegistry = [];
1769
1763
  // Dependency tracking: "type:name" -> dependencies
@@ -2338,6 +2332,67 @@ var _MetadataManager = class _MetadataManager {
2338
2332
  * result may be memoized depends on what happened to the read's registration
2339
2333
  * while it ran, which only `list()` can see.
2340
2334
  */
2335
+ /**
2336
+ * Merge one loader's answer for `type` into `items`, under the identity that
2337
+ * loader holds each item by.
2338
+ *
2339
+ * ## [#14205] The identity of a loader-held item is its ROW KEY
2340
+ *
2341
+ * Both plural readers used to key a loader's items by `body.name`, and admit
2342
+ * an item only when the body carried a string one:
2343
+ *
2344
+ * ```ts
2345
+ * if (itemAny && typeof itemAny.name === 'string' && !items.has(itemAny.name))
2346
+ * ```
2347
+ *
2348
+ * A body is not required to name itself. `register(type, name, data)` takes
2349
+ * the key as its ARGUMENT, and `assertMetadataRegisterContract` says in as
2350
+ * many words that "A document with NO `name` of its own is fine — the argument
2351
+ * is the key". An aggregated `defineView` container is exactly that: no own
2352
+ * `name` by design, identity carried in the row's `name` column.
2353
+ *
2354
+ * So the old gate dropped every such item the moment the registry went cold
2355
+ * and only the loader could answer — a persisted view container vanished from
2356
+ * `list('view')` after a restart, and `listDiagnosed()` called the short
2357
+ * answer complete because no loader had thrown. Same gate, same effect, in
2358
+ * `listForIndex()`: a nameless `api` row fell out of the endpoint index, where
2359
+ * a miss reads as "nothing declares this route".
2360
+ *
2361
+ * The repair is to ask the loader for the key instead of guessing it from the
2362
+ * body ({@link MetadataLoader.loadManyKeyed}), and to keep the key BESIDE the
2363
+ * body: nothing is written into a body that deliberately has none, so the
2364
+ * register contract's refusal of a disagreeing `data.name` still means what it
2365
+ * says.
2366
+ *
2367
+ * Nothing consumers see today changes shape. For any item that went through
2368
+ * `register()`, a `data.name` that exists is required to EQUAL the key, so the
2369
+ * keyed merge produces the identical map entry; what is new is only the
2370
+ * entries the old gate refused. The `loadMany()` fallback below is the
2371
+ * pre-#14205 behaviour verbatim, for loaders that cannot produce keys
2372
+ * (`RemoteLoader`'s wire format carries bodies only).
2373
+ *
2374
+ * Read failures are NOT caught here: `readListUncached` warns-and-continues,
2375
+ * `listForIndex` deliberately throws, and that difference is each caller's to
2376
+ * keep.
2377
+ */
2378
+ async admitLoaderItems(loader, type, items) {
2379
+ if (typeof loader.loadManyKeyed === "function") {
2380
+ const keyed = await loader.loadManyKeyed(type);
2381
+ for (const entry of keyed) {
2382
+ if (!entry || typeof entry.name !== "string" || entry.name === "") continue;
2383
+ if (items.has(entry.name)) continue;
2384
+ items.set(entry.name, entry.data);
2385
+ }
2386
+ return;
2387
+ }
2388
+ const loaderItems = await loader.loadMany(type);
2389
+ for (const item of loaderItems) {
2390
+ const itemAny = item;
2391
+ if (itemAny && typeof itemAny.name === "string" && !items.has(itemAny.name)) {
2392
+ items.set(itemAny.name, item);
2393
+ }
2394
+ }
2395
+ }
2341
2396
  async readListUncached(type) {
2342
2397
  const items = /* @__PURE__ */ new Map();
2343
2398
  const typeStore = this.registry.get(type);
@@ -2350,13 +2405,7 @@ var _MetadataManager = class _MetadataManager {
2350
2405
  const errors = [];
2351
2406
  for (const loader of this.loaders.values()) {
2352
2407
  try {
2353
- const loaderItems = await loader.loadMany(type);
2354
- for (const item of loaderItems) {
2355
- const itemAny = item;
2356
- if (itemAny && typeof itemAny.name === "string" && !items.has(itemAny.name)) {
2357
- items.set(itemAny.name, item);
2358
- }
2359
- }
2408
+ await this.admitLoaderItems(loader, type, items);
2360
2409
  this.reportLoaderReadRecovered(loader.contract.name);
2361
2410
  } catch (e) {
2362
2411
  degraded = true;
@@ -2509,13 +2558,7 @@ var _MetadataManager = class _MetadataManager {
2509
2558
  }
2510
2559
  }
2511
2560
  for (const loader of this.loaders.values()) {
2512
- const loaderItems = await loader.loadMany(type);
2513
- for (const item of loaderItems) {
2514
- const itemAny = item;
2515
- if (itemAny && typeof itemAny.name === "string" && !items.has(itemAny.name)) {
2516
- items.set(itemAny.name, item);
2517
- }
2518
- }
2561
+ await this.admitLoaderItems(loader, type, items);
2519
2562
  }
2520
2563
  return Array.from(items.values());
2521
2564
  }
@@ -2760,12 +2803,54 @@ var _MetadataManager = class _MetadataManager {
2760
2803
  * Runtime-authored `shared` / `personal` views (`sys_view_definition`) are
2761
2804
  * merged in by the REST layer; this method returns the `package` layer that
2762
2805
  * was registered from source.
2806
+ *
2807
+ * ## [#13913] Aggregated containers are expanded inline, per read
2808
+ *
2809
+ * `this.list('view')` is `MetadataManager`'s OWN loader-based store — the
2810
+ * in-memory registry plus every registered loader — and is a completely
2811
+ * different store from the `sys_metadata` rows `getMetaItems` reads. #13407
2812
+ * taught `getMetaItems` to expand a runtime-authored aggregated container
2813
+ * inline; this exit never called it and had no equivalent step, so a
2814
+ * container that `GET /meta/view?object=` now serves still answered **empty**
2815
+ * here.
2816
+ *
2817
+ * Merely getting the container into the store would not have helped: the
2818
+ * filter also requires `viewKind`, and a container has none. Loosening that
2819
+ * requirement is NOT the repair — it would answer with the container itself
2820
+ * as a view, the behaviour #7163 ruled wrong — so what is added below is the
2821
+ * container's **expansion**, whose items each carry the `viewKind` + `object`
2822
+ * pair this filter has always tested. The filter itself is untouched: it
2823
+ * reads the top-level `object`, exactly as `ViewSchema.object` declares.
2824
+ *
2825
+ * Registry-free and per-read, mirroring #13407's choice at the other exit and
2826
+ * for the same reason — the registry is process-wide, so a read must not
2827
+ * graft rows into it (see `view-container-expansion.ts`'s header, which also
2828
+ * records why the protocol's copy of this logic cannot be imported).
2829
+ *
2830
+ * Already-present items win: an expansion contributes only names the store
2831
+ * does not already hold, so a container whose expanded ViewItems were
2832
+ * registered by a source registrar (the ObjectQL boot loop, the artifact/HMR
2833
+ * loader) still answers with those registered, fully-enriched items and this
2834
+ * step adds nothing.
2763
2835
  */
2764
2836
  async getViewsByObject(object) {
2765
2837
  const views = await this.list("view");
2766
- return views.filter(
2838
+ const matches = views.filter(
2767
2839
  (v) => v && typeof v === "object" && v.viewKind && v.object === object
2768
- ).sort(
2840
+ );
2841
+ const known = /* @__PURE__ */ new Set();
2842
+ for (const v of views) {
2843
+ if (v && typeof v === "object" && typeof v.name === "string") known.add(v.name);
2844
+ }
2845
+ for (const v of views) {
2846
+ for (const item of expandRuntimeViewContainer(v)) {
2847
+ if (!item.viewKind || item.object !== object) continue;
2848
+ if (known.has(item.name)) continue;
2849
+ known.add(item.name);
2850
+ matches.push(item);
2851
+ }
2852
+ }
2853
+ return matches.sort(
2769
2854
  (a, b) => (a.order ?? 0) - (b.order ?? 0) || String(a.name).localeCompare(String(b.name))
2770
2855
  );
2771
2856
  }
@@ -3183,66 +3268,18 @@ var _MetadataManager = class _MetadataManager {
3183
3268
  };
3184
3269
  }
3185
3270
  // ==========================================
3186
- // Overlay / Customization Management
3271
+ // Overlay / Customization Management — REMOVED (#13135, ADR-0049)
3187
3272
  // ==========================================
3188
- overlayKey(type, name, scope = "platform") {
3189
- return `${encodeURIComponent(type)}:${encodeURIComponent(name)}:${scope}`;
3190
- }
3191
- /**
3192
- * Get the active overlay for a metadata item
3193
- */
3194
- async getOverlay(type, name, scope) {
3195
- return this.overlays.get(this.overlayKey(type, name, scope ?? "platform"));
3196
- }
3197
- /**
3198
- * Save/update an overlay for a metadata item
3199
- */
3200
- async saveOverlay(overlay) {
3201
- if (this.config.persistence?.overlayWritable === false) {
3202
- const msg = `MetadataManager overlays are read-only (persistence.overlayWritable=false); refusing to save overlay for ${overlay.baseType}/${overlay.baseName}`;
3203
- if (this.config.validation?.throwOnError) {
3204
- throw new Error(msg);
3205
- }
3206
- this.logger.warn(msg);
3207
- return;
3208
- }
3209
- const key = this.overlayKey(overlay.baseType, overlay.baseName, overlay.scope);
3210
- this.overlays.set(key, overlay);
3211
- }
3212
- /**
3213
- * Remove an overlay, reverting to the base definition
3214
- */
3215
- async removeOverlay(type, name, scope) {
3216
- this.overlays.delete(this.overlayKey(type, name, scope ?? "platform"));
3217
- }
3218
- /**
3219
- * Get the effective (merged) metadata after applying all overlays.
3220
- * Resolution order: system ← merge(platform) ← merge(user)
3221
- */
3222
- async getEffective(type, name, context) {
3223
- const base = await this.get(type, name);
3224
- if (!base) return void 0;
3225
- let effective = { ...base };
3226
- const platformOverlay = await this.getOverlay(type, name, "platform");
3227
- if (platformOverlay?.active && platformOverlay.patch) {
3228
- effective = { ...effective, ...platformOverlay.patch };
3229
- }
3230
- if (context?.userId) {
3231
- const userOverlayKey = this.overlayKey(type, name, "user") + `:${context.userId}`;
3232
- const userOverlay = this.overlays.get(userOverlayKey) ?? await this.getOverlay(type, name, "user");
3233
- if (userOverlay?.active && userOverlay.patch) {
3234
- if (!userOverlay.owner || userOverlay.owner === context.userId) {
3235
- effective = { ...effective, ...userOverlay.patch };
3236
- }
3237
- }
3238
- } else {
3239
- const userOverlay = await this.getOverlay(type, name, "user");
3240
- if (userOverlay?.active && userOverlay.patch && !userOverlay.owner) {
3241
- effective = { ...effective, ...userOverlay.patch };
3242
- }
3243
- }
3244
- return effective;
3245
- }
3273
+ //
3274
+ // The in-memory overlay limb (`getOverlay` / `saveOverlay` / `removeOverlay`
3275
+ // / `getEffective`, keyed `type:name:scope`) implemented the paper
3276
+ // metadata-customization protocol removed from `@objectstack/spec` in the
3277
+ // same change: no route ever served the paper `.../overlay` or
3278
+ // `.../effective` endpoints, and the only callers of these methods were this
3279
+ // package's own unit tests. ADR-0126 supersedes the protocol on the record.
3280
+ // The org-scoped customization that actually ships is ADR-0005's
3281
+ // `sys_metadata` overlay (`getMetaItemLayered` in metadata-protocol), which
3282
+ // never lived here.
3246
3283
  // ==========================================
3247
3284
  // Watch / Subscribe (IMetadataService)
3248
3285
  // ==========================================
@@ -4016,6 +4053,7 @@ var MetadataManager = _MetadataManager;
4016
4053
  // src/plugin.ts
4017
4054
  var import_promises = require("fs/promises");
4018
4055
  var import_node_crypto2 = require("crypto");
4056
+ var import_core2 = require("@objectstack/core");
4019
4057
 
4020
4058
  // src/node-metadata-manager.ts
4021
4059
  var path2 = __toESM(require("path"), 1);
@@ -4026,7 +4064,11 @@ var fs = __toESM(require("fs/promises"), 1);
4026
4064
  var path = __toESM(require("path"), 1);
4027
4065
  var import_glob = require("glob");
4028
4066
  var import_node_crypto = require("crypto");
4029
- var FilesystemLoader = class {
4067
+ function ownNameOf(data) {
4068
+ const own = data?.name;
4069
+ return typeof own === "string" && own !== "" ? own : null;
4070
+ }
4071
+ var _FilesystemLoader = class _FilesystemLoader {
4030
4072
  constructor(rootDir, serializers, logger) {
4031
4073
  this.rootDir = rootDir;
4032
4074
  this.serializers = serializers;
@@ -4124,6 +4166,82 @@ var FilesystemLoader = class {
4124
4166
  }
4125
4167
  }
4126
4168
  async loadMany(type, options) {
4169
+ return (await this.loadManyEntries(type, options)).map((entry) => entry.data);
4170
+ }
4171
+ /**
4172
+ * [#14341] The keyed half of {@link loadMany} — see {@link MetadataKeyedItem}
4173
+ * for why the store's key travels BESIDE the body instead of being folded
4174
+ * into it.
4175
+ *
4176
+ * THE RULE, in one sentence: an item is keyed by this loader's own
4177
+ * name-to-path derivation — {@link nameFromFilename}, the very basename
4178
+ * derivation `list()` reports — ONLY where that derivation is a bijection for
4179
+ * the file (it sits directly under `ROOT/TYPE/` and carries one of the
4180
+ * extensions {@link findFile} tries, so `findFile(type, key)` resolves back to
4181
+ * this same file); every other shape keeps the pre-#14205 behaviour verbatim,
4182
+ * keyed by `body.name` when it has one and dropped when it has none.
4183
+ *
4184
+ * Why the rule stops there (PM ruling on #14341, 2026-09-02, knowingly over
4185
+ * triage's "a nested path keeps whatever `list()` reports for it today"):
4186
+ * `list()` and `findFile()` DISAGREE outside that shape. For
4187
+ * `ROOT/TYPE/crm/account.json`, `list()` reports the bare `account`, but
4188
+ * `findFile()` resolves that name against `ROOT/TYPE/account.json` and finds
4189
+ * nothing — the only name reaching the file is `crm/account`, which nothing
4190
+ * reports. An extension-less file is read by `loadMany()` and reported by
4191
+ * `list()`, and `findFile()` resolves neither. Keying by either side would
4192
+ * mint a name some other door cannot open, and two directories holding the
4193
+ * same basename would collide in silence
4194
+ * (`MetadataManager.admitLoaderItems()` keeps the first and says nothing).
4195
+ * The card's own fence: "keying items under names nothing else uses … is
4196
+ * worse than today's honest drop". So the drop stays exactly where the key is
4197
+ * unsettled, and is pinned as a RECORD in
4198
+ * `filesystem-loader-keyed-items.test.ts`.
4199
+ *
4200
+ * [#14486, partial] `list()` and {@link findFile} have since converged on
4201
+ * {@link resolvableNameForPath} — the derivation this method already used —
4202
+ * so a nested or extension-less file is now neither listed nor resolvable.
4203
+ * What did NOT change is the WALK behind this method: `loadManyEntries()`
4204
+ * still READS those files, so `loadMany()` still returns their bodies and
4205
+ * this method still falls back to `body.name` for them. That half of the
4206
+ * #14486 ruling ("nothing unlisted is returned by `loadMany()` either") is
4207
+ * deliberately NOT taken here: it would invert the three landed #14341 pins
4208
+ * in `filesystem-loader-keyed-items.test.ts:113,167,187` and the
4209
+ * `loadMany()` CONTROL at `:196`, and that file was under a concurrent
4210
+ * claim (PR #14627) when this landed. The remaining divergence — listed ⊂
4211
+ * loaded — is pinned as a RECORD in
4212
+ * `filesystem-loader-list-reachability.test.ts` rather than left implicit.
4213
+ *
4214
+ * One consequence, deliberate: a flat file whose `body.name` DISAGREES with
4215
+ * its basename is now keyed by the BASENAME. That is #14205's rule (identity
4216
+ * is the key the store holds an item under, not `body.name`) applied to this
4217
+ * loader, and it aligns `MetadataManager.list()` with `listNames()` for that
4218
+ * shape.
4219
+ *
4220
+ * The body is handed back by reference, unchanged: nothing is written into a
4221
+ * body that deliberately has no `name`. `limit` bounds the items LOADED,
4222
+ * exactly as `loadMany()` does — an entry the key rule drops has still been
4223
+ * read and still counts against it.
4224
+ */
4225
+ async loadManyKeyed(type, options) {
4226
+ const typeDir = path.join(this.rootDir, type);
4227
+ const keyed = [];
4228
+ for (const entry of await this.loadManyEntries(type, options)) {
4229
+ const name = this.resolvableNameForPath(typeDir, entry.file) ?? ownNameOf(entry.data);
4230
+ if (name) {
4231
+ keyed.push({ name, data: entry.data });
4232
+ }
4233
+ }
4234
+ return keyed;
4235
+ }
4236
+ /**
4237
+ * The single walk behind {@link loadMany} and {@link loadManyKeyed}: one glob,
4238
+ * one serializer pass, one `limit`. Shared so the two can never answer with
4239
+ * different bodies for the same file — {@link MetadataLoader.loadManyKeyed}
4240
+ * requires `data` to be "the same body `loadMany()` would return for the
4241
+ * item", and a second copy of this walk is how that would quietly stop being
4242
+ * true.
4243
+ */
4244
+ async loadManyEntries(type, options) {
4127
4245
  const { patterns = ["**/*"], recursive: _recursive = true, limit } = options || {};
4128
4246
  const typeDir = path.join(this.rootDir, type);
4129
4247
  const items = [];
@@ -4146,7 +4264,7 @@ var FilesystemLoader = class {
4146
4264
  const serializer = this.getSerializer(format);
4147
4265
  if (serializer) {
4148
4266
  const data = serializer.deserialize(content);
4149
- items.push(data);
4267
+ items.push({ file, data });
4150
4268
  }
4151
4269
  } catch (error) {
4152
4270
  this.logger?.warn("Failed to load file", {
@@ -4200,6 +4318,30 @@ var FilesystemLoader = class {
4200
4318
  return null;
4201
4319
  }
4202
4320
  }
4321
+ /**
4322
+ * [#14486] The names this loader can be asked for, and ONLY those: a file
4323
+ * directly under `ROOT/TYPE/` carrying an extension one of this instance's
4324
+ * REGISTERED serializers claims. Every name it reports resolves back through
4325
+ * {@link findFile}, so `listNames()` and `get()` give the same answer.
4326
+ *
4327
+ * It used to report `path.basename(file, ext)` for every file the glob found,
4328
+ * nested or not, extension or not — and {@link findFile} resolves neither
4329
+ * shape. `ROOT/TYPE/crm/account.json` was listed as `account`, which resolves
4330
+ * against `ROOT/TYPE/account.json` and finds nothing; an extension-less
4331
+ * `ROOT/TYPE/noext` was listed as `noext`, which resolves under no appended
4332
+ * extension at all. A name in the list that `get()` answers `null` for is the
4333
+ * silent failure an author (human or AI) reads as their own typo, so they
4334
+ * retry the same word: the list and the door now agree instead.
4335
+ *
4336
+ * Ruling (maintainer, via the director seat on #14486, 2026-09-02): narrow
4337
+ * the list — direction A, over B (reverse-unify: report `crm/account` and
4338
+ * teach `findFile()` path-shaped names), which would have made a slash inside
4339
+ * a metadata name every consumer's permanent obligation with no measured
4340
+ * demand for it. The two-segment layout follows ADR-0008 §10, which
4341
+ * `metadata-fs`'s `parseItemPath()` already enforces for its own store; the
4342
+ * EXTENSION set deliberately does NOT follow §10's `.json`-only rule — see
4343
+ * {@link resolvableExtensions} for why.
4344
+ */
4203
4345
  async list(type) {
4204
4346
  const typeDir = path.join(this.rootDir, type);
4205
4347
  try {
@@ -4208,11 +4350,7 @@ var FilesystemLoader = class {
4208
4350
  ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*"],
4209
4351
  nodir: true
4210
4352
  });
4211
- return files.map((file) => {
4212
- const ext = path.extname(file);
4213
- const basename3 = path.basename(file, ext);
4214
- return basename3;
4215
- });
4353
+ return files.map((file) => this.resolvableNameForPath(typeDir, path.join(typeDir, file))).filter((name) => name !== null);
4216
4354
  } catch (error) {
4217
4355
  this.logger?.error("Failed to list", void 0, {
4218
4356
  type,
@@ -4290,12 +4428,66 @@ var FilesystemLoader = class {
4290
4428
  throw error;
4291
4429
  }
4292
4430
  }
4431
+ /**
4432
+ * [#14486] The extensions a name can be resolved under, for THIS instance:
4433
+ * the ones belonging to the serializer set it was constructed with. Shared by
4434
+ * {@link findFile}, {@link resolvableNameForPath} and therefore {@link list},
4435
+ * so the set a name can be RESOLVED under cannot drift from the set that is
4436
+ * LISTED or the set {@link loadManyKeyed} is willing to KEY by.
4437
+ *
4438
+ * Registered, not hard-coded, and deliberately not ADR-0008 §10's `.json`
4439
+ * only. §10 governs the `metadata-fs` store; applying it verbatim here would
4440
+ * drop `.yaml` and `.ts` metadata out of `listNames()` — a breakage this card
4441
+ * never asked for. Under the manager's DEFAULT format set
4442
+ * (`typescript` / `json` / `yaml`, `metadata-manager.ts`) that leaves `.js`
4443
+ * out, which is the card's row-4 membership mismatch closing for free: a `.js`
4444
+ * file was listed and resolvable while `loadMany()` could never return it and
4445
+ * `load()` threw `No serializer found for format: javascript`. Register
4446
+ * `javascript` and it is listed, resolvable and loadable together.
4447
+ */
4448
+ resolvableExtensions() {
4449
+ const extensions = [];
4450
+ for (const [format, formatExtensions] of _FilesystemLoader.EXTENSIONS_BY_FORMAT) {
4451
+ if (this.serializers.has(format)) {
4452
+ extensions.push(...formatExtensions);
4453
+ }
4454
+ }
4455
+ return extensions;
4456
+ }
4457
+ /**
4458
+ * The metadata name this loader reports for a file: the basename with its
4459
+ * extension stripped. One derivation, shared by {@link list} and
4460
+ * {@link loadManyKeyed}, so the two cannot drift for the shape where they
4461
+ * agree — `dotted.config.json` is `dotted.config` for both.
4462
+ */
4463
+ static nameFromFilename(file) {
4464
+ return path.basename(file, path.extname(file));
4465
+ }
4466
+ /**
4467
+ * The key for a file IF this loader's name-to-path mapping is a bijection for
4468
+ * it: a file directly under `ROOT/TYPE/` carrying an extension
4469
+ * {@link findFile} tries, so `findFile(type, key)` resolves back to this very
4470
+ * file. `null` for every other shape — a nested path, an extension-less file,
4471
+ * an extension spelled in a case `findFile()` does not compose — which is why
4472
+ * {@link loadManyKeyed} falls back to `body.name` there rather than minting a
4473
+ * key no other door can open.
4474
+ */
4475
+ resolvableNameForPath(typeDir, file) {
4476
+ const rel = path.relative(typeDir, file);
4477
+ if (rel === "" || rel.split(path.sep).length !== 1) {
4478
+ return null;
4479
+ }
4480
+ if (!this.resolvableExtensions().includes(path.extname(rel))) {
4481
+ return null;
4482
+ }
4483
+ return _FilesystemLoader.nameFromFilename(rel);
4484
+ }
4293
4485
  /**
4294
4486
  * Find file for a given type and name
4295
4487
  */
4296
4488
  async findFile(type, name) {
4297
4489
  const typeDir = path.join(this.rootDir, type);
4298
- const extensions = [".json", ".yaml", ".yml", ".ts", ".js"];
4490
+ const extensions = this.resolvableExtensions();
4299
4491
  for (const ext of extensions) {
4300
4492
  const filePath = path.join(typeDir, `${name}${ext}`);
4301
4493
  try {
@@ -4341,6 +4533,19 @@ var FilesystemLoader = class {
4341
4533
  return `"${hash}"`;
4342
4534
  }
4343
4535
  };
4536
+ /**
4537
+ * The inverse of {@link detectFormat}: which file extensions carry which
4538
+ * format. Fixed ORDER, because it is also {@link findFile}'s precedence when
4539
+ * two files under one type directory share a stem — registration order must
4540
+ * not be able to change which file `ROOT/TYPE/NAME` opens.
4541
+ */
4542
+ _FilesystemLoader.EXTENSIONS_BY_FORMAT = [
4543
+ ["json", [".json"]],
4544
+ ["yaml", [".yaml", ".yml"]],
4545
+ ["typescript", [".ts"]],
4546
+ ["javascript", [".js"]]
4547
+ ];
4548
+ var FilesystemLoader = _FilesystemLoader;
4344
4549
 
4345
4550
  // src/node-metadata-manager.ts
4346
4551
  var NodeMetadataManager = class extends MetadataManager {
@@ -4465,6 +4670,20 @@ var MemoryLoader = class {
4465
4670
  if (!typeStore) return [];
4466
4671
  return Array.from(typeStore.values());
4467
4672
  }
4673
+ /**
4674
+ * [#14205] The keyed half of {@link loadMany}. The storage map is already
4675
+ * `Type -> Name -> Data`, so the key this loader holds an item under is the
4676
+ * map key — `loadMany()` was simply discarding it, which dropped every
4677
+ * nameless body out of `MetadataManager.list()` and out of the endpoint index.
4678
+ *
4679
+ * The body is handed back by reference, unchanged: the key travels beside it,
4680
+ * never folded into it.
4681
+ */
4682
+ async loadManyKeyed(type, _options) {
4683
+ const typeStore = this.storage.get(type);
4684
+ if (!typeStore) return [];
4685
+ return Array.from(typeStore, ([name, data]) => ({ name, data }));
4686
+ }
4468
4687
  async exists(type, name) {
4469
4688
  return this.storage.get(type)?.has(name) ?? false;
4470
4689
  }
@@ -4511,20 +4730,20 @@ var MemoryLoader = class {
4511
4730
 
4512
4731
  // src/plugin.ts
4513
4732
  var import_kernel2 = require("@objectstack/spec/kernel");
4514
- var import_shared2 = require("@objectstack/spec/shared");
4515
- var import_metadata_core2 = require("@objectstack/metadata-core");
4516
- var import_spec2 = require("@objectstack/spec");
4733
+ var import_shared3 = require("@objectstack/spec/shared");
4734
+ var import_metadata_core3 = require("@objectstack/metadata-core");
4517
4735
  var import_spec3 = require("@objectstack/spec");
4736
+ var import_spec4 = require("@objectstack/spec");
4518
4737
  var queryableMetadataObjects = [
4519
- import_metadata_core2.SysMetadataObject,
4520
- import_metadata_core2.SysMetadataHistoryObject,
4738
+ import_metadata_core3.SysMetadataObject,
4739
+ import_metadata_core3.SysMetadataHistoryObject,
4521
4740
  // ADR-0067 commit log — sibling of sys_metadata_history (see note above).
4522
- import_metadata_core2.SysMetadataCommitObject,
4523
- import_metadata_core2.SysMetadataAuditObject,
4741
+ import_metadata_core3.SysMetadataCommitObject,
4742
+ import_metadata_core3.SysMetadataAuditObject,
4524
4743
  // Runtime view storage (shared / personal). Must always be provisioned so
4525
4744
  // end-user view creation via the generic data API has a place to write —
4526
4745
  // mirroring why sys_metadata is always provisioned for PUT /meta.
4527
- import_metadata_core2.SysViewDefinitionObject
4746
+ import_metadata_core3.SysViewDefinitionObject
4528
4747
  ];
4529
4748
  var REPO_SUBDIR = ".objectstack/metadata";
4530
4749
  var ARTIFACT_FIELD_TO_TYPE = {
@@ -4547,8 +4766,37 @@ var ARTIFACT_FIELD_TO_TYPE = {
4547
4766
  // positions from artifact ingestion.
4548
4767
  positions: "position",
4549
4768
  permissions: "permission",
4769
+ // [ADR-0066 D1] `capabilities` reaches the door at #12892 step 1, the
4770
+ // maintainer's `option 1` ruling ("the door owns the registration
4771
+ // route" for the five artifact security collections). Until #12894
4772
+ // measured it, `AppPlugin`'s `SECURITY_FIELDS` block
4773
+ // (packages/runtime/src/app-plugin.ts) was this collection's SOLE
4774
+ // registrar on an artifact boot — the one security collection the door
4775
+ // could not reach — so a declared capability was registered from bytes
4776
+ // nothing strict-parses, with no schema default and no ADR-0010
4777
+ // provenance. Measured on the two-reader harness, the door's copy adds
4778
+ // exactly four keys the raw copy lacks: `scope` (the schema default)
4779
+ // and `_packageId` / `_packageVersion` / `_provenance`.
4780
+ //
4781
+ // ⚠️ This entry makes the door a SECOND writer, not yet the only one:
4782
+ // `AppPlugin` still registers `capabilities`, and it runs last, so the
4783
+ // raw copy still wins a real artifact boot. Step 2 of the ruling (that
4784
+ // block stops registering these five on the artifact path, after a
4785
+ // census of the non-artifact boot paths) is what makes this the only
4786
+ // copy. Until then the divergence is the interim reality the ruling
4787
+ // explicitly permits, and #12878's pins are what keep it visible.
4788
+ capabilities: "capability",
4550
4789
  sharingRules: "sharing_rule",
4551
- policies: "policy",
4790
+ // `policies: 'policy'` removed at #12894: the stack schema is a
4791
+ // `strictObject` that declares no top-level `policies` key, so a
4792
+ // definition carrying one is refused by the strict parse a few lines
4793
+ // below — the entry could never match, and nothing was ever registered
4794
+ // under `policy` from this map. The word is real, but it lives ONE LEVEL
4795
+ // DOWN: on a permission set it is an alias for `rowLevelSecurity`
4796
+ // (`PERMISSION_SET_KEY_ALIASES`, packages/spec/src/security/permission.zod.ts)
4797
+ // — a key on an ITEM, never a collection. Third retirement of this exact
4798
+ // shape in this map (`themes` and `roles` above); the reasons are kept
4799
+ // in place because the first two are what made this one findable.
4552
4800
  apis: "api",
4553
4801
  webhooks: "webhook",
4554
4802
  agents: "agent",
@@ -4591,6 +4839,21 @@ var MetadataPlugin = class {
4591
4839
  * degrades on purpose (objects are discovered via the legacy fallback).
4592
4840
  */
4593
4841
  this.optionalDependencies = ["com.objectstack.engine.objectql"];
4842
+ /**
4843
+ * Once-per-process dedupe for the summaries the versioned artifact window
4844
+ * emits. The artifact watcher replays `_parseAndRegisterArtifact` on every
4845
+ * file change, so without this a dev loop over a legacy artifact would
4846
+ * re-announce the same finding on every reload — the same shape
4847
+ * `Protocol.storedConversionWarned` guards on the stored-row pass, which
4848
+ * this surfacing is modeled on.
4849
+ *
4850
+ * Two key families share the set, because they share the replay:
4851
+ * `<conversionId>|<label>` for a forward-conversion summary (#12772), and
4852
+ * `unbound-form-predicate-root|<label>` for the unbound-root notice
4853
+ * (#12915) — one line per artifact there, not one per conversion, since
4854
+ * the notice already aggregates every finding it made.
4855
+ */
4856
+ this.artifactConversionWarned = /* @__PURE__ */ new Set();
4594
4857
  this.init = async (ctx) => {
4595
4858
  this.initCtx = ctx;
4596
4859
  ctx.logger.info("Initializing Metadata Manager", {
@@ -4599,7 +4862,6 @@ var MetadataPlugin = class {
4599
4862
  artifactSource: this.options.artifactSource?.mode
4600
4863
  });
4601
4864
  ctx.registerService("metadata", this.manager);
4602
- console.log("[MetadataPlugin] Registered metadata service, has getRegisteredTypes:", typeof this.manager.getRegisteredTypes);
4603
4865
  const registerSysObjects = this.options.registerSystemObjects !== false;
4604
4866
  if (registerSysObjects) {
4605
4867
  try {
@@ -4621,7 +4883,7 @@ var MetadataPlugin = class {
4621
4883
  }
4622
4884
  ctx.logger.info("MetadataPlugin providing metadata service (primary mode)", {
4623
4885
  mode: this.options.artifactSource?.mode ?? "file-system",
4624
- features: ["watch", "multi-format", "query", "overlay", "type-registry"]
4886
+ features: ["watch", "multi-format", "query", "type-registry"]
4625
4887
  });
4626
4888
  };
4627
4889
  this.start = async (ctx) => {
@@ -4704,7 +4966,7 @@ var MetadataPlugin = class {
4704
4966
  if (httpServer && typeof httpServer.getRawApp === "function") {
4705
4967
  const { registerMetadataHmrRoutes: registerMetadataHmrRoutes2 } = await Promise.resolve().then(() => (init_hmr_routes(), hmr_routes_exports));
4706
4968
  const hub = registerMetadataHmrRoutes2(httpServer.getRawApp(), this.manager);
4707
- hub.setOnPostReload(async (body = {}) => {
4969
+ hub?.setOnPostReload(async (body = {}) => {
4708
4970
  const src3 = this.options.artifactSource;
4709
4971
  if (src3?.mode === "local-file") {
4710
4972
  try {
@@ -4744,7 +5006,7 @@ var MetadataPlugin = class {
4744
5006
  pending = true;
4745
5007
  try {
4746
5008
  await this._reloadAndAnnounce(ctx, src2, [src2.path]);
4747
- hub.broadcastReload("artifact-file-changed", [src2.path]);
5009
+ hub?.broadcastReload("artifact-file-changed", [src2.path]);
4748
5010
  ctx.logger.info("[MetadataPlugin] artifact auto-reloaded (file watcher)", {
4749
5011
  path: src2.path
4750
5012
  });
@@ -4766,7 +5028,13 @@ var MetadataPlugin = class {
4766
5028
  ctx.logger.warn("[MetadataPlugin] artifact watcher failed to start", { error: e?.message });
4767
5029
  }
4768
5030
  }
4769
- console.log("[MetadataPlugin] HMR endpoint registered at /api/v1/dev/metadata-events");
5031
+ if (hub) {
5032
+ console.log("[MetadataPlugin] HMR endpoint registered at /api/v1/dev/metadata-events");
5033
+ } else {
5034
+ console.log(
5035
+ `[MetadataPlugin] dev metadata-HMR endpoints NOT mounted \u2014 they require NODE_ENV=development (this process: ${process.env.NODE_ENV ? `NODE_ENV=${process.env.NODE_ENV}` : "NODE_ENV unset, treated as production"})`
5036
+ );
5037
+ }
4770
5038
  } else {
4771
5039
  console.log("[MetadataPlugin] HTTP server with getRawApp() not available \u2014 skipping HMR endpoint");
4772
5040
  }
@@ -4862,6 +5130,106 @@ var MetadataPlugin = class {
4862
5130
  if (timer) clearTimeout(timer);
4863
5131
  }
4864
5132
  }
5133
+ /**
5134
+ * Versioned ADR-0087 forward conversion at the artifact-ingestion door
5135
+ * (#12772) — runs BEFORE the strict schema parse below, because the parse
5136
+ * is the refusal point.
5137
+ *
5138
+ * A compiled artifact is data at rest with a version stamp: built by
5139
+ * released tooling, then unchanged while the platform moves on. When a
5140
+ * spec release retires an authorable key inside a protocol line (spec
5141
+ * 17.1 → 17.2 retired the `allowRestore`/`allowPurge` permission bits),
5142
+ * every already-built artifact carrying the key becomes unbootable at the
5143
+ * tombstone — with no operator remedy, since `os migrate meta` targets
5144
+ * sources, not built artifacts. The stored-row read path already replays
5145
+ * the conversion chain for exactly this reason
5146
+ * (`applyConversionsToStoredItem`, ADR-0087 addendum); this is the same
5147
+ * policy at the artifact door, **keyed off the artifact's own declared
5148
+ * `engines.protocol` floor**: an artifact authored below the running spec
5149
+ * version converts forward, an artifact authored at the current (or a
5150
+ * newer) surface converts nothing and answers to the strict parse,
5151
+ * tombstones included. The version key is what keeps this a conversion
5152
+ * rather than an amnesty — the retired keys return with the M2 lifecycle
5153
+ * batch (#1883), and artifacts authored against that surface must never
5154
+ * have them stripped by history.
5155
+ *
5156
+ * Notices surface the way the stored-row pass's do — operator-visible and
5157
+ * deduped — as one summary line per conversion per artifact rather than
5158
+ * one per rewritten path (a real 17.1 artifact carried 150 strips of the
5159
+ * same two keys; 150 identical warn lines would bury the boot log).
5160
+ */
5161
+ _convertArtifactForward(ctx, definition, label) {
5162
+ const result = (0, import_metadata_core3.applyArtifactForwardConversions)(definition);
5163
+ this._warnUnboundFormPredicateRoots(ctx, result, label);
5164
+ if (result.notices.length === 0) return result.definition;
5165
+ const byConversion = /* @__PURE__ */ new Map();
5166
+ for (const n of result.notices) {
5167
+ const existing = byConversion.get(n.conversionId);
5168
+ if (existing) existing.count += 1;
5169
+ else byConversion.set(n.conversionId, { count: 1, firstPath: n.path, message: n.message });
5170
+ }
5171
+ for (const [conversionId, agg] of byConversion) {
5172
+ const key = `${conversionId}|${label}`;
5173
+ if (this.artifactConversionWarned.has(key)) continue;
5174
+ this.artifactConversionWarned.add(key);
5175
+ ctx.logger.warn(
5176
+ `[MetadataPlugin] artifact '${label}' predates this runtime's spec (authored engines.protocol floor ${result.authoredFloor ?? "<undeclared>"}, runtime spec ${result.runtimeSpecVersion}) \u2014 converted ${agg.count} site(s) forward via ADR-0087 conversion '${conversionId}' (first at ${agg.firstPath}). ${agg.message} The artifact file itself is unchanged \u2014 rebuild it with current tooling ('os build') to persist the canonical shape.`
5177
+ );
5178
+ }
5179
+ return result.definition;
5180
+ }
5181
+ /**
5182
+ * Operator-facing boot notice for form-view predicates that fault OPEN on
5183
+ * this runtime (#12915 scope C — maintainer ruling 2026-08-28, 「同意C」).
5184
+ *
5185
+ * A form-view predicate binds `record` / `previous` / `parent` (runtime
5186
+ * record forms) or `data` (metadata-editing forms) — and a FIELD-level one
5187
+ * also binds `current_user` and its ADR-0068 aliases (objectui#6010),
5188
+ * which a SECTION-level one does not. The contract states beside that
5189
+ * vocabulary that a bare identifier is UNBOUND and the predicate faults,
5190
+ * and `visibleWhen`'s fault fallback is `true`. On a real
5191
+ * 17.1-built artifact that combination dead-ends record creation in the
5192
+ * console: the conditionally hidden field renders, and its unconditional
5193
+ * `required: true` — authored to be gated by the visibility that no longer
5194
+ * applies — blocks every submit, while the same payload POSTs 201 through
5195
+ * REST. Nothing refused, nothing logged, and only the operator can fix it
5196
+ * (by rebuilding the artifact), so this is the channel the ruling picked:
5197
+ * service startup, server-side, never a console surface — the person at
5198
+ * the form cannot act on "your artifact is stale".
5199
+ *
5200
+ * **Detection only.** No refusal, no rewrite, no behaviour change: the
5201
+ * predicate keeps faulting open exactly as before. Rewriting a bare root to
5202
+ * `record.` is the ADR-0087 conversion (#12915 scope A), deferred by the
5203
+ * same ruling with an explicit start line.
5204
+ *
5205
+ * **Same versioned window as the conversion replay above** — and read off
5206
+ * that pass's own verdict rather than recomputed, so the two can never
5207
+ * disagree about which artifacts are "old". An artifact declaring the
5208
+ * current (or a newer) floor answers to the strict parse and gets nothing
5209
+ * from here even when it does carry bare roots; that boundary is what keeps
5210
+ * a notice about legacy artifacts out of contract territory. An undeclared
5211
+ * range is treated as old data at rest, matching the grandfathering posture
5212
+ * the window already takes (`converted-undeclared`).
5213
+ */
5214
+ _warnUnboundFormPredicateRoots(ctx, result, label) {
5215
+ if (result.verdict !== "converted-forward" && result.verdict !== "converted-undeclared") return;
5216
+ const findings = (0, import_metadata_core3.detectUnboundFormViewPredicateRoots)(result.definition);
5217
+ if (findings.length === 0) return;
5218
+ const key = `unbound-form-predicate-root|${label}`;
5219
+ if (this.artifactConversionWarned.has(key)) return;
5220
+ this.artifactConversionWarned.add(key);
5221
+ const views = [...new Set(findings.map((f) => f.view))];
5222
+ const roots = [...new Set(findings.map((f) => f.root))];
5223
+ const quote = (list) => list.map((v) => `'${v}'`).join(", ");
5224
+ const surfaces = new Set(findings.map((f) => f.surface));
5225
+ const vocabulary = [
5226
+ surfaces.has("field") ? `on a form FIELD: ${quote(import_metadata_core3.BOUND_FORM_FIELD_PREDICATE_ROOTS)}` : null,
5227
+ surfaces.has("section") ? `on a form SECTION: ${quote(import_metadata_core3.BOUND_FORM_VIEW_PREDICATE_ROOTS)}` : null
5228
+ ].filter(Boolean).join("; ");
5229
+ ctx.logger.warn(
5230
+ `[MetadataPlugin] artifact '${label}' predates this runtime's spec (authored engines.protocol floor ${result.authoredFloor ?? "<undeclared>"}, runtime spec ${result.runtimeSpecVersion}) and carries ${findings.length} form-view predicate(s) whose root identifier is NOT bound where it evaluates \u2014 ${quote(roots)} (bound roots ${vocabulary}) \u2014 across ${views.length} view(s): ${views.join(", ")} (first at ${findings[0].path}). Each one faults at evaluation and visibility fails OPEN, so a field the predicate was authored to HIDE renders anyway \u2014 and an unconditional 'required: true' on such a field dead-ends record creation in the console while the REST door still accepts it. Nothing was rewritten and no behaviour changed here; rebuild the artifact with current tooling ('os build') so its predicates carry a bound root ('record.<field>').`
5231
+ );
5232
+ }
4865
5233
  /**
4866
5234
  * Parse raw artifact JSON (envelope or bare definition) and register all
4867
5235
  * metadata items into the MetadataManager.
@@ -4880,16 +5248,22 @@ var MetadataPlugin = class {
4880
5248
  let metadata;
4881
5249
  const obj = raw;
4882
5250
  if (obj?.schemaVersion && obj?.commitId && obj?.metadata !== void 0) {
4883
- const artifact = EnvironmentArtifactSchema.parse(obj);
5251
+ const artifact = EnvironmentArtifactSchema.parse({
5252
+ ...obj,
5253
+ metadata: this._convertArtifactForward(ctx, obj.metadata, label)
5254
+ });
4884
5255
  metadata = artifact.metadata;
4885
5256
  } else if (obj?.success && obj?.data?.metadata) {
4886
- const artifact = EnvironmentArtifactSchema.parse(obj.data);
5257
+ const artifact = EnvironmentArtifactSchema.parse({
5258
+ ...obj.data,
5259
+ metadata: this._convertArtifactForward(ctx, obj.data.metadata, label)
5260
+ });
4887
5261
  metadata = artifact.metadata;
4888
5262
  } else {
4889
- const def = ObjectStackDefinitionSchema.parse(obj);
5263
+ const def = ObjectStackDefinitionSchema.parse(this._convertArtifactForward(ctx, obj, label));
4890
5264
  const canonical = JSON.stringify(def, Object.keys(def).sort());
4891
5265
  const checksum = (0, import_node_crypto2.createHash)("sha256").update(canonical).digest("hex");
4892
- const environmentId = this.options.environmentId ?? "proj_local";
5266
+ const environmentId = this.options.environmentId ?? "env_local";
4893
5267
  EnvironmentArtifactSchema.parse({
4894
5268
  schemaVersion: "0.1",
4895
5269
  environmentId,
@@ -4903,53 +5277,127 @@ var MetadataPlugin = class {
4903
5277
  const memLoader = new MemoryLoader();
4904
5278
  const manifestPackageId = metadata?.manifest?.id ?? metadata?.id ?? void 0;
4905
5279
  const manifestVersion = metadata?.manifest?.version ?? metadata?.version ?? void 0;
5280
+ const carriesPackages = Array.isArray(metadata?.packages);
5281
+ const bodies = (0, import_core2.resolveArtifactPackageOrder)(metadata);
5282
+ const ownedByPackage = /* @__PURE__ */ new Map();
5283
+ const claim = (type, name) => {
5284
+ let names = ownedByPackage.get(type);
5285
+ if (!names) ownedByPackage.set(type, names = /* @__PURE__ */ new Set());
5286
+ names.add(name);
5287
+ };
5288
+ const claimed = (type, name) => ownedByPackage.get(type)?.has(name) === true;
5289
+ let totalRegistered = 0;
5290
+ for (const body of bodies) {
5291
+ totalRegistered += await this._registerArtifactBodyCollections(
5292
+ ctx,
5293
+ memLoader,
5294
+ body,
5295
+ carriesPackages ? {
5296
+ packageId: (0, import_core2.artifactPackageId)(body),
5297
+ packageVersion: body?.version ?? void 0
5298
+ } : { packageId: manifestPackageId, packageVersion: manifestVersion },
5299
+ { claim: carriesPackages ? claim : void 0 }
5300
+ );
5301
+ }
5302
+ if (carriesPackages) {
5303
+ const residual = await this._registerArtifactBodyCollections(
5304
+ ctx,
5305
+ memLoader,
5306
+ metadata,
5307
+ { packageId: manifestPackageId, packageVersion: manifestVersion },
5308
+ { skip: claimed }
5309
+ );
5310
+ totalRegistered += residual;
5311
+ if (residual > 0) {
5312
+ ctx.logger.warn(
5313
+ `[MetadataPlugin] artifact '${label}' carries ${residual} top-level metadata item(s) that none of its ${bodies.length} package bodies declare. They were registered under the artifact's own manifest id ('${manifestPackageId ?? "<none>"}') because no package in the artifact claims them, so every door will report that id as their owner. Rebuild the artifact so each collection it ships is carried by the package that owns it.`
5314
+ );
5315
+ }
5316
+ }
5317
+ this.manager.registerLoader(memLoader);
5318
+ ctx.logger.info("[MetadataPlugin] Artifact metadata loaded", { source: label, totalRegistered });
5319
+ return totalRegistered;
5320
+ }
5321
+ /**
5322
+ * Register ONE artifact body's collections into the MetadataManager.
5323
+ *
5324
+ * A "body" is either the whole artifact (the single-package branch, where
5325
+ * the artifact and its one package are the same object) or one entry of
5326
+ * `packages[]` (ADR-0130 D4), which is an assembled
5327
+ * `{ ...manifest, ...collections }` payload carrying the same collection
5328
+ * keys the top level does. The loop is identical for both — that is the
5329
+ * point: there is one ingestion of a collection here, not one per shape.
5330
+ *
5331
+ * @param provenance - The `(packageId, packageVersion)` every item found in
5332
+ * this body is stamped with (ADR-0010 §3.7, via `applyProtection`). It is
5333
+ * the body's OWN identity, never the enclosing artifact's, which is what
5334
+ * makes a multi-package artifact's items agree with the registry and with
5335
+ * `GET /api/v1/packages` about who owns them.
5336
+ * @param slots.claim - Called with every `(type, name)` this pass
5337
+ * registered. Passed when reading package bodies; the residual sweep uses
5338
+ * what it recorded.
5339
+ * @param slots.skip - Consulted before registering each `(type, name)`.
5340
+ * Passed ONLY by the residual sweep, so a package body's copy is never
5341
+ * overwritten by the flattened top-level copy of the same definition —
5342
+ * the overwrite that re-attributed the item to the artifact's manifest.
5343
+ * ⛔ It is never passed while reading the bodies themselves: two items of
5344
+ * one name inside one body still register as they always have (last
5345
+ * wins), because suppressing that would be a behaviour change on the
5346
+ * single-package branch D7 pins.
5347
+ * @returns How many items this body registered.
5348
+ */
5349
+ async _registerArtifactBodyCollections(ctx, memLoader, body, provenance, slots = {}) {
5350
+ const { packageId, packageVersion } = provenance;
4906
5351
  let totalRegistered = 0;
4907
5352
  for (const [field, metaType] of Object.entries(ARTIFACT_FIELD_TO_TYPE)) {
4908
- const items = metadata[field];
5353
+ const items = body[field];
4909
5354
  if (!Array.isArray(items) || items.length === 0) continue;
4910
5355
  for (const item of items) {
4911
- if (metaType === "view" && (0, import_spec3.isAggregatedViewContainer)(item)) {
4912
- const viewObject = item?.list?.data?.object ?? item?.form?.data?.object;
5356
+ if (metaType === "view" && (0, import_spec4.isAggregatedViewContainer)(item)) {
5357
+ const viewObject = deriveViewContainerObject(item);
4913
5358
  if (!viewObject) continue;
4914
- (0, import_shared2.applyProtection)(item, {
4915
- packageId: manifestPackageId,
4916
- packageVersion: manifestVersion
5359
+ if (slots.skip?.("view", viewObject)) continue;
5360
+ (0, import_shared3.applyProtection)(item, {
5361
+ packageId,
5362
+ packageVersion
4917
5363
  });
4918
5364
  await memLoader.save("view", viewObject, item);
4919
5365
  await this.manager.register("view", viewObject, item, { notify: false });
4920
5366
  totalRegistered++;
4921
- for (const vi of (0, import_spec3.expandViewContainer)(viewObject, item)) {
5367
+ slots.claim?.("view", viewObject);
5368
+ for (const vi of (0, import_spec4.expandViewContainer)(viewObject, item)) {
4922
5369
  for (const w of vi._diagnostics?.warnings ?? []) {
4923
5370
  ctx.logger.warn(`[MetadataPlugin] View expansion warning for '${vi.name}': ${w.message}`);
4924
5371
  }
4925
- (0, import_shared2.applyProtection)(vi, {
4926
- packageId: manifestPackageId,
4927
- packageVersion: manifestVersion
5372
+ (0, import_shared3.applyProtection)(vi, {
5373
+ packageId,
5374
+ packageVersion
4928
5375
  });
4929
5376
  await memLoader.save("view", vi.name, vi);
4930
5377
  await this.manager.register("view", vi.name, vi, { notify: false });
4931
5378
  totalRegistered++;
5379
+ slots.claim?.("view", vi.name);
4932
5380
  }
4933
5381
  continue;
4934
5382
  }
4935
5383
  let name = item?.name;
4936
5384
  if (!name) {
4937
5385
  if (metaType === "view") {
4938
- name = item?.list?.data?.object ?? item?.form?.data?.object;
5386
+ name = deriveViewContainerObject(item);
4939
5387
  }
4940
5388
  }
4941
5389
  if (!name) continue;
4942
- (0, import_shared2.applyProtection)(item, {
4943
- packageId: manifestPackageId,
4944
- packageVersion: manifestVersion
5390
+ if (slots.skip?.(metaType, name)) continue;
5391
+ (0, import_shared3.applyProtection)(item, {
5392
+ packageId,
5393
+ packageVersion
4945
5394
  });
4946
5395
  await memLoader.save(metaType, name, item);
4947
5396
  await this.manager.register(metaType, name, item, { notify: false });
4948
5397
  totalRegistered++;
5398
+ slots.claim?.(metaType, name);
4949
5399
  }
4950
5400
  }
4951
- this.manager.registerLoader(memLoader);
4952
- ctx.logger.info("[MetadataPlugin] Artifact metadata loaded", { source: label, totalRegistered });
4953
5401
  return totalRegistered;
4954
5402
  }
4955
5403
  /**
@@ -5025,7 +5473,7 @@ var MetadataPlugin = class {
5025
5473
  for (const item of items) {
5026
5474
  const meta = item;
5027
5475
  if (meta?.name) {
5028
- (0, import_shared2.applyProtection)(meta, {
5476
+ (0, import_shared3.applyProtection)(meta, {
5029
5477
  packageId: this.options.packageId
5030
5478
  });
5031
5479
  await this.manager.register(entry.type, meta.name, item, { notify: false });
@@ -5143,7 +5591,7 @@ var RemoteLoader = class {
5143
5591
  };
5144
5592
 
5145
5593
  // src/index.ts
5146
- var import_metadata_core3 = require("@objectstack/metadata-core");
5594
+ var import_metadata_core4 = require("@objectstack/metadata-core");
5147
5595
 
5148
5596
  // src/utils/history-cleanup.ts
5149
5597
  var import_kernel3 = require("@objectstack/spec/kernel");
@@ -5430,6 +5878,7 @@ var MigrationExecutor = class {
5430
5878
  TypeScriptSerializer,
5431
5879
  YAMLSerializer,
5432
5880
  calculateChecksum,
5881
+ deriveViewContainerObject,
5433
5882
  generateDiffSummary,
5434
5883
  generateSimpleDiff
5435
5884
  });