@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/node.js CHANGED
@@ -16,9 +16,14 @@ var __export = (target, all) => {
16
16
  // src/routes/hmr-routes.ts
17
17
  var hmr_routes_exports = {};
18
18
  __export(hmr_routes_exports, {
19
+ isDevMetadataEndpointEnabled: () => isDevMetadataEndpointEnabled,
19
20
  registerMetadataHmrRoutes: () => registerMetadataHmrRoutes
20
21
  });
22
+ function isDevMetadataEndpointEnabled(env = process.env) {
23
+ return (env.NODE_ENV ?? "").trim().toLowerCase() === "development";
24
+ }
21
25
  function registerMetadataHmrRoutes(app, manager, options = {}) {
26
+ if (!isDevMetadataEndpointEnabled()) return null;
22
27
  const routePath = options.path ?? "/api/v1/dev/metadata-events";
23
28
  const listeners = /* @__PURE__ */ new Set();
24
29
  const broadcast = (evt) => {
@@ -351,7 +356,7 @@ export default metadata;
351
356
  };
352
357
 
353
358
  // src/loaders/database-loader.ts
354
- import { SysMetadataObject, SysMetadataHistoryObject } from "@objectstack/metadata-core";
359
+ import { SysMetadataObject as SysMetadataObject2, SysMetadataHistoryObject as SysMetadataHistoryObject2 } from "@objectstack/metadata-core";
355
360
  import { applyConversionsToStoredItem } from "@objectstack/spec";
356
361
  import { PLURAL_TO_SINGULAR } from "@objectstack/spec/shared";
357
362
 
@@ -519,141 +524,49 @@ var LRUCache = class {
519
524
  }
520
525
  };
521
526
 
522
- // src/utils/schema-sync-errors.ts
523
- import { isRelationSubObjectPhrase } from "@objectstack/types";
524
- var ALREADY_EXISTS = {
525
- codes: /* @__PURE__ */ new Set([
526
- // PostgreSQL SQLSTATE (class 42 — syntax error or access rule violation)
527
- "42P07",
528
- // duplicate_table
529
- "42701",
530
- // duplicate_column
531
- "42710",
532
- // duplicate_object — index / constraint already exists
533
- // MySQL / MariaDB (mysql2 puts the symbolic name on `code`)
534
- "ER_TABLE_EXISTS_ERROR",
535
- // 1050
536
- "ER_DUP_FIELDNAME",
537
- // 1060
538
- "ER_DUP_KEYNAME"
539
- // 1061
540
- ]),
541
- errnos: /* @__PURE__ */ new Set([1050, 1060, 1061]),
542
- /**
543
- * Message fallback for drivers that carry no machine-readable code —
544
- * notably SQLite, whose `code` is the undifferentiated `SQLITE_ERROR` for
545
- * every DDL failure, so the message is the only signal available:
546
- * - `table sys_metadata already exists`
547
- * - `duplicate column name: environment_id`
548
- * - `index idx_x already exists`
549
- * Postgres phrases its own as `relation "x" already exists` /
550
- * `column "x" of relation "y" already exists`, which matches the same test.
551
- */
552
- message: /already exists|duplicate column name|duplicate key name/i
553
- };
554
- var MISSING_TABLE = {
555
- codes: /* @__PURE__ */ new Set([
556
- "42P01",
557
- // PostgreSQL undefined_table
558
- "ER_NO_SUCH_TABLE"
559
- // MySQL / MariaDB 1146
560
- ]),
561
- errnos: /* @__PURE__ */ new Set([1146]),
562
- /**
563
- * - SQLite / libsql: `no such table: sys_metadata_history`
564
- * - PostgreSQL: `relation "sys_metadata_history" does not exist`
565
- * - MySQL/MariaDB: `Table 'app.sys_metadata_history' doesn't exist`
566
- */
567
- message: /no such table|relation ["'`][^"'`]+["'`] does not exist|table ["'`][^"'`]+["'`] doesn'?t exist|unknown table/i,
568
- excludes: {
569
- /**
570
- * Exactly the three SQLSTATEs the docblock above already names as
571
- * must-stay-loud neighbours of `does not exist`. They are listed here
572
- * rather than merely trusted to miss the message test, because two of
573
- * them (42703 columns, 42704 constraints/triggers) have a phrasing that
574
- * *does* hit it, and because a code is a fact where prose is a guess.
575
- *
576
- * Postgres-shaped on purpose: measured, neither MySQL
577
- * (`Unknown column 'label' in 'field list'`) nor SQLite
578
- * (`no such column: bogus`, `table t has no column named label`)
579
- * phrases a sub-object failure so that a missing-table phrase falls out
580
- * of it, so there is nothing there to exclude. Adding their codes would
581
- * be surface with no defect behind it.
582
- */
583
- codes: /* @__PURE__ */ new Set([
584
- "42703",
585
- // undefined_column
586
- "42704",
587
- // undefined_object — constraint, trigger, role, type, …
588
- "3D000"
589
- // invalid_catalog_name — `database "x" does not exist`
590
- ]),
591
- /**
592
- * `«sub-object» "x" of relation "y" …` — Postgres' phrasing for a
593
- * failure about something *inside* a relation, which therefore says the
594
- * relation itself is present. The two in-repo siblings that carry this
595
- * phrase are `mapDataError` (`packages/rest`, #5352) and
596
- * `service-analytics`'s missing-column subtraction (#6035/PR #6346).
597
- *
598
- * [#6615] All three now read one home — `@objectstack/types` — instead
599
- * of three hand-kept copies, so the phrase can no longer be taught to
600
- * the repo a fourth time or drift in one package only. The **width**
601
- * difference that used to justify the copy is preserved and is the
602
- * reason the home exports two functions rather than one: those two
603
- * *extract* the column name to phrase a better error, so a miss costs a
604
- * vaguer message; this one *excludes*, so a miss restores the
605
- * corruption. {@link isRelationSubObjectPhrase} is therefore the wider
606
- * question — it drops their `column`/`[a-z0-9_]+`/`does not exist`
607
- * anchors: any sub-object, any quoted identifier, any verdict.
608
- * Over-matching here only ever converts a benign verdict into a loud
609
- * one, which is the direction this whole module already errs in.
610
- */
611
- matchesMessage: isRelationSubObjectPhrase
527
+ // src/loaders/database-loader.ts
528
+ import { isMissingTableError, isSchemaAlreadyExistsError } from "@objectstack/types";
529
+
530
+ // src/migrations/driver-exec.ts
531
+ function resolveDriverExec(driver) {
532
+ const candidate = driver;
533
+ if (!candidate) return void 0;
534
+ if (typeof candidate.execute === "function") {
535
+ return (sql, bindings) => candidate.execute(sql, bindings ? [...bindings] : []);
612
536
  }
613
- };
614
- var MAX_CAUSE_DEPTH = 4;
615
- function matchesDriverError(error, signature, depth) {
616
- if (error === null || error === void 0 || depth > MAX_CAUSE_DEPTH) return false;
617
- if (typeof error === "string") {
618
- if (signature.excludes?.matchesMessage(error)) return false;
619
- return signature.message.test(error);
620
- }
621
- if (typeof error !== "object") return false;
622
- const err = error;
623
- const excludes = signature.excludes;
624
- if (excludes) {
625
- if (typeof err.code === "string" && excludes.codes.has(err.code)) return false;
626
- if (typeof err.message === "string" && excludes.matchesMessage(err.message)) return false;
627
- }
628
- if (typeof err.code === "string" && signature.codes.has(err.code)) return true;
629
- if (typeof err.errno === "number" && signature.errnos.has(err.errno)) return true;
630
- if (typeof err.message === "string" && signature.message.test(err.message)) return true;
631
- return matchesDriverError(err.cause, signature, depth + 1);
632
- }
633
- function isSchemaAlreadyExistsError(error, depth = 0) {
634
- return matchesDriverError(error, ALREADY_EXISTS, depth);
537
+ if (typeof candidate.raw === "function") {
538
+ return (sql, bindings) => candidate.raw(sql, bindings ? [...bindings] : []);
539
+ }
540
+ return void 0;
635
541
  }
636
- function isMissingTableError(error, depth = 0) {
637
- return matchesDriverError(error, MISSING_TABLE, depth);
542
+ function driverExecRefusal(helper) {
543
+ 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.`;
638
544
  }
639
545
 
640
546
  // src/migrations/migrate-project-id-to-environment-id.ts
641
- var AFFECTED_TABLES = [
642
- "sys_metadata",
643
- "sys_metadata_history"
644
- ];
547
+ import { SysMetadataObject, SysMetadataHistoryObject } from "@objectstack/metadata-core";
548
+ var SOURCE_COLUMN = "project_id";
549
+ var TARGET_COLUMN = "environment_id";
550
+ var CANDIDATE_OBJECTS = [SysMetadataObject, SysMetadataHistoryObject];
551
+ function declaresColumn(object, column) {
552
+ return Object.prototype.hasOwnProperty.call(object.fields ?? {}, column);
553
+ }
554
+ var CANDIDATE_TABLES = CANDIDATE_OBJECTS.map((o) => o.name);
555
+ var AFFECTED_TABLES = CANDIDATE_OBJECTS.filter((o) => declaresColumn(o, TARGET_COLUMN)).map((o) => o.name);
645
556
  async function migrateProjectIdToEnvironmentId(driver) {
646
- const driverAny = driver;
647
- if (typeof driverAny.raw !== "function") {
648
- throw new Error(
649
- "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."
650
- );
557
+ const exec = resolveDriverExec(driver);
558
+ if (!exec) {
559
+ throw new Error(driverExecRefusal("migrateProjectIdToEnvironmentId"));
651
560
  }
652
561
  const results = [];
653
- for (const table of AFFECTED_TABLES) {
562
+ for (const table of CANDIDATE_TABLES) {
563
+ if (!AFFECTED_TABLES.includes(table)) {
564
+ results.push({ table, status: "skipped_not_declared" });
565
+ continue;
566
+ }
654
567
  try {
655
- const hasColumn = await _columnExists(driverAny, table, "project_id");
656
- const alreadyMigrated = await _columnExists(driverAny, table, "environment_id");
568
+ const hasColumn = await _columnExists(exec, table, SOURCE_COLUMN);
569
+ const alreadyMigrated = await _columnExists(exec, table, TARGET_COLUMN);
657
570
  if (alreadyMigrated && !hasColumn) {
658
571
  results.push({ table, status: "already_done" });
659
572
  continue;
@@ -662,8 +575,8 @@ async function migrateProjectIdToEnvironmentId(driver) {
662
575
  results.push({ table, status: "table_missing" });
663
576
  continue;
664
577
  }
665
- await driverAny.raw(
666
- `ALTER TABLE "${table}" RENAME COLUMN project_id TO environment_id`
578
+ await exec(
579
+ `ALTER TABLE "${table}" RENAME COLUMN ${SOURCE_COLUMN} TO ${TARGET_COLUMN}`
667
580
  );
668
581
  results.push({ table, status: "renamed" });
669
582
  } catch (err) {
@@ -672,14 +585,14 @@ async function migrateProjectIdToEnvironmentId(driver) {
672
585
  }
673
586
  return results;
674
587
  }
675
- async function _columnExists(driver, table, column) {
588
+ async function _columnExists(exec, table, column) {
676
589
  try {
677
- const rows = await driver.raw(`PRAGMA table_info("${table}")`);
590
+ const rows = await exec(`PRAGMA table_info("${table}")`);
678
591
  if (Array.isArray(rows) && rows.length > 0) {
679
592
  const list2 = Array.isArray(rows[0]) ? rows[0] : rows;
680
593
  return list2.some((r) => r?.name === column);
681
594
  }
682
- const result = await driver.raw(
595
+ const result = await exec(
683
596
  `SELECT column_name FROM information_schema.columns WHERE table_name = ? AND column_name = ?`,
684
597
  [table, column]
685
598
  );
@@ -691,6 +604,16 @@ async function _columnExists(driver, table, column) {
691
604
  }
692
605
 
693
606
  // src/loaders/database-loader.ts
607
+ function canonicalIsoInstant(value) {
608
+ if (value === null || value === void 0) return void 0;
609
+ if (value instanceof Date) return value.toISOString();
610
+ if (typeof value === "string") return value;
611
+ return String(value);
612
+ }
613
+ function isoFromValidDate(value) {
614
+ if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString();
615
+ return value;
616
+ }
694
617
  var DatabaseLoader = class {
695
618
  constructor(options) {
696
619
  this.contract = {
@@ -827,9 +750,11 @@ var DatabaseLoader = class {
827
750
  }
828
751
  return this.driver.create(table, data);
829
752
  }
753
+ // `null` is the driver path's not-found answer (`IDataDriver.update()`,
754
+ // #13878); both callers here resolve the row first and discard the result.
830
755
  async _update(table, id, data) {
831
756
  if (this.engine) {
832
- return this.engine.update(table, { id, ...data });
757
+ return this.engine.update(table, { ...data, id });
833
758
  }
834
759
  return this.driver.update(table, id, data);
835
760
  }
@@ -874,7 +799,7 @@ var DatabaseLoader = class {
874
799
  }
875
800
  return max + 1;
876
801
  } catch (error) {
877
- if (isMissingTableError(error)) return 1;
802
+ if (isMissingTableError(error, this.historyTableName)) return 1;
878
803
  throw error;
879
804
  }
880
805
  }
@@ -912,7 +837,7 @@ var DatabaseLoader = class {
912
837
  }
913
838
  try {
914
839
  await this.driver.syncSchema(this.tableName, {
915
- ...SysMetadataObject,
840
+ ...SysMetadataObject2,
916
841
  name: this.tableName
917
842
  });
918
843
  } catch (error) {
@@ -951,7 +876,7 @@ var DatabaseLoader = class {
951
876
  }
952
877
  try {
953
878
  await this.driver.syncSchema(this.historyTableName, {
954
- ...SysMetadataHistoryObject,
879
+ ...SysMetadataHistoryObject2,
955
880
  name: this.historyTableName
956
881
  });
957
882
  if (this.historySchemaFailureReported) {
@@ -1120,9 +1045,9 @@ var DatabaseLoader = class {
1120
1045
  source: row.source,
1121
1046
  tags: row.tags ? typeof row.tags === "string" ? JSON.parse(row.tags) : row.tags : void 0,
1122
1047
  createdBy: row.created_by,
1123
- createdAt: row.created_at,
1048
+ createdAt: isoFromValidDate(row.created_at),
1124
1049
  updatedBy: row.updated_by,
1125
- updatedAt: row.updated_at
1050
+ updatedAt: isoFromValidDate(row.updated_at)
1126
1051
  };
1127
1052
  }
1128
1053
  // ==========================================
@@ -1173,7 +1098,7 @@ var DatabaseLoader = class {
1173
1098
  * with its empty value.
1174
1099
  */
1175
1100
  rethrowUnlessTableUnprovisioned(error) {
1176
- if (isMissingTableError(error)) return;
1101
+ if (isMissingTableError(error, this.tableName)) return;
1177
1102
  throw error;
1178
1103
  }
1179
1104
  // ==========================================
@@ -1223,17 +1148,38 @@ var DatabaseLoader = class {
1223
1148
  };
1224
1149
  }
1225
1150
  }
1226
- async loadMany(type, _options) {
1151
+ /**
1152
+ * The one type-wide read both plural readers share: every row of `type`, each
1153
+ * body paired with the `name` COLUMN it was stored under.
1154
+ *
1155
+ * [#14205] `name` is `null` only for a row whose key column does not hold a
1156
+ * string. Such a row is still a body {@link loadMany} must return — dropping
1157
+ * it would change what consumers see today — but it has no usable identity,
1158
+ * so {@link loadManyKeyed} filters it out rather than invent one.
1159
+ *
1160
+ * One query and one cache entry serve both methods: `loadMany()` used to own
1161
+ * them, and splitting them would have made every keyed `list()` read miss the
1162
+ * cache and re-hit the database.
1163
+ */
1164
+ async readTypeRows(type) {
1227
1165
  await this.ensureSchema();
1228
1166
  if (this.loadManyCache) {
1229
1167
  const cached = this.loadManyCache.get(type);
1230
- if (cached !== void 0) return cached;
1168
+ if (cached !== void 0) {
1169
+ return cached;
1170
+ }
1231
1171
  }
1232
1172
  try {
1233
1173
  const rows = await this._find(this.tableName, {
1234
1174
  where: this.baseFilter(type)
1235
1175
  });
1236
- const result = rows.map((row) => this.rowToData(row)).filter((data) => data !== null);
1176
+ const result = [];
1177
+ for (const row of rows) {
1178
+ const data = this.rowToData(row);
1179
+ if (data === null) continue;
1180
+ const name = row.name;
1181
+ result.push({ name: typeof name === "string" && name !== "" ? name : null, data });
1182
+ }
1237
1183
  this.loadManyCache?.set(type, result);
1238
1184
  return result;
1239
1185
  } catch (error) {
@@ -1241,6 +1187,29 @@ var DatabaseLoader = class {
1241
1187
  return [];
1242
1188
  }
1243
1189
  }
1190
+ async loadMany(type, _options) {
1191
+ return (await this.readTypeRows(type)).map((entry) => entry.data);
1192
+ }
1193
+ /**
1194
+ * [#14205] The keyed half of {@link loadMany} — see
1195
+ * {@link MetadataKeyedItem} for why the row key travels beside the body
1196
+ * instead of inside it.
1197
+ *
1198
+ * `DatabaseLoader` is where the defect was measured: an aggregated view
1199
+ * container is written by `register('view', OBJECT, container)` and stored
1200
+ * verbatim, so its `sys_metadata` row carries the identity in the `name`
1201
+ * COLUMN and the body has none. {@link rowToData} returns that body without
1202
+ * folding the column in — deliberately, and unchanged here.
1203
+ */
1204
+ async loadManyKeyed(type, _options) {
1205
+ const entries = await this.readTypeRows(type);
1206
+ const keyed = [];
1207
+ for (const entry of entries) {
1208
+ if (entry.name === null) continue;
1209
+ keyed.push({ name: entry.name, data: entry.data });
1210
+ }
1211
+ return keyed;
1212
+ }
1244
1213
  async exists(type, name) {
1245
1214
  await this.ensureSchema();
1246
1215
  if (this.loadCache) {
@@ -1276,7 +1245,7 @@ var DatabaseLoader = class {
1276
1245
  const metadataStr = typeof row.metadata === "string" ? row.metadata : JSON.stringify(row.metadata);
1277
1246
  const stats = {
1278
1247
  size: metadataStr.length,
1279
- mtime: record.updatedAt ?? record.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1248
+ mtime: canonicalIsoInstant(record.updatedAt ?? record.createdAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
1280
1249
  format: "json",
1281
1250
  etag: record.checksum
1282
1251
  };
@@ -1337,7 +1306,7 @@ var DatabaseLoader = class {
1337
1306
  changeNote: row.change_note,
1338
1307
  organizationId: row.organization_id,
1339
1308
  recordedBy: row.recorded_by,
1340
- recordedAt: row.recorded_at
1309
+ recordedAt: isoFromValidDate(row.recorded_at)
1341
1310
  };
1342
1311
  }
1343
1312
  /**
@@ -1394,7 +1363,7 @@ var DatabaseLoader = class {
1394
1363
  changeNote: row.change_note,
1395
1364
  organizationId: row.organization_id,
1396
1365
  recordedBy: row.recorded_by,
1397
- recordedAt: row.recorded_at
1366
+ recordedAt: isoFromValidDate(row.recorded_at)
1398
1367
  };
1399
1368
  });
1400
1369
  return { records: result, total, hasMore };
@@ -1694,6 +1663,35 @@ var EndpointMatcher = class {
1694
1663
  }
1695
1664
  };
1696
1665
 
1666
+ // src/view-container-expansion.ts
1667
+ import {
1668
+ expandViewContainer,
1669
+ isAggregatedViewContainer
1670
+ } from "@objectstack/spec";
1671
+ import { applyProtection } from "@objectstack/spec/shared";
1672
+ function deriveViewContainerObject(container) {
1673
+ if (!container || typeof container !== "object") return void 0;
1674
+ const c = container;
1675
+ const own = typeof c.object === "string" && c.object ? c.object : void 0;
1676
+ const byName = typeof c.name === "string" && c.name ? c.name : void 0;
1677
+ return own ?? c?.list?.data?.object ?? c?.form?.data?.object ?? byName;
1678
+ }
1679
+ function expandRuntimeViewContainer(data) {
1680
+ if (!isAggregatedViewContainer(data)) return [];
1681
+ const container = data;
1682
+ const viewObject = deriveViewContainerObject(container);
1683
+ if (!viewObject) return [];
1684
+ const out = [];
1685
+ for (const vi of expandViewContainer(viewObject, container)) {
1686
+ applyProtection(vi, {
1687
+ packageId: container._packageId,
1688
+ packageVersion: container._packageVersion
1689
+ });
1690
+ out.push(vi);
1691
+ }
1692
+ return out;
1693
+ }
1694
+
1697
1695
  // src/metadata-manager.ts
1698
1696
  var WRITABLE_LOADER_METHODS = ["save", "delete"];
1699
1697
  var WRITABLE_LOADER_METHOD_SIGNATURE = {
@@ -1733,8 +1731,6 @@ var _MetadataManager = class _MetadataManager {
1733
1731
  this.watchCallbacks = /* @__PURE__ */ new Map();
1734
1732
  // In-memory metadata registry: type -> name -> data
1735
1733
  this.registry = /* @__PURE__ */ new Map();
1736
- // Overlay storage: "type:name:scope" -> MetadataOverlay
1737
- this.overlays = /* @__PURE__ */ new Map();
1738
1734
  // Type registry for metadata type info
1739
1735
  this.typeRegistry = [];
1740
1736
  // Dependency tracking: "type:name" -> dependencies
@@ -2309,6 +2305,67 @@ var _MetadataManager = class _MetadataManager {
2309
2305
  * result may be memoized depends on what happened to the read's registration
2310
2306
  * while it ran, which only `list()` can see.
2311
2307
  */
2308
+ /**
2309
+ * Merge one loader's answer for `type` into `items`, under the identity that
2310
+ * loader holds each item by.
2311
+ *
2312
+ * ## [#14205] The identity of a loader-held item is its ROW KEY
2313
+ *
2314
+ * Both plural readers used to key a loader's items by `body.name`, and admit
2315
+ * an item only when the body carried a string one:
2316
+ *
2317
+ * ```ts
2318
+ * if (itemAny && typeof itemAny.name === 'string' && !items.has(itemAny.name))
2319
+ * ```
2320
+ *
2321
+ * A body is not required to name itself. `register(type, name, data)` takes
2322
+ * the key as its ARGUMENT, and `assertMetadataRegisterContract` says in as
2323
+ * many words that "A document with NO `name` of its own is fine — the argument
2324
+ * is the key". An aggregated `defineView` container is exactly that: no own
2325
+ * `name` by design, identity carried in the row's `name` column.
2326
+ *
2327
+ * So the old gate dropped every such item the moment the registry went cold
2328
+ * and only the loader could answer — a persisted view container vanished from
2329
+ * `list('view')` after a restart, and `listDiagnosed()` called the short
2330
+ * answer complete because no loader had thrown. Same gate, same effect, in
2331
+ * `listForIndex()`: a nameless `api` row fell out of the endpoint index, where
2332
+ * a miss reads as "nothing declares this route".
2333
+ *
2334
+ * The repair is to ask the loader for the key instead of guessing it from the
2335
+ * body ({@link MetadataLoader.loadManyKeyed}), and to keep the key BESIDE the
2336
+ * body: nothing is written into a body that deliberately has none, so the
2337
+ * register contract's refusal of a disagreeing `data.name` still means what it
2338
+ * says.
2339
+ *
2340
+ * Nothing consumers see today changes shape. For any item that went through
2341
+ * `register()`, a `data.name` that exists is required to EQUAL the key, so the
2342
+ * keyed merge produces the identical map entry; what is new is only the
2343
+ * entries the old gate refused. The `loadMany()` fallback below is the
2344
+ * pre-#14205 behaviour verbatim, for loaders that cannot produce keys
2345
+ * (`RemoteLoader`'s wire format carries bodies only).
2346
+ *
2347
+ * Read failures are NOT caught here: `readListUncached` warns-and-continues,
2348
+ * `listForIndex` deliberately throws, and that difference is each caller's to
2349
+ * keep.
2350
+ */
2351
+ async admitLoaderItems(loader, type, items) {
2352
+ if (typeof loader.loadManyKeyed === "function") {
2353
+ const keyed = await loader.loadManyKeyed(type);
2354
+ for (const entry of keyed) {
2355
+ if (!entry || typeof entry.name !== "string" || entry.name === "") continue;
2356
+ if (items.has(entry.name)) continue;
2357
+ items.set(entry.name, entry.data);
2358
+ }
2359
+ return;
2360
+ }
2361
+ const loaderItems = await loader.loadMany(type);
2362
+ for (const item of loaderItems) {
2363
+ const itemAny = item;
2364
+ if (itemAny && typeof itemAny.name === "string" && !items.has(itemAny.name)) {
2365
+ items.set(itemAny.name, item);
2366
+ }
2367
+ }
2368
+ }
2312
2369
  async readListUncached(type) {
2313
2370
  const items = /* @__PURE__ */ new Map();
2314
2371
  const typeStore = this.registry.get(type);
@@ -2321,13 +2378,7 @@ var _MetadataManager = class _MetadataManager {
2321
2378
  const errors = [];
2322
2379
  for (const loader of this.loaders.values()) {
2323
2380
  try {
2324
- const loaderItems = await loader.loadMany(type);
2325
- for (const item of loaderItems) {
2326
- const itemAny = item;
2327
- if (itemAny && typeof itemAny.name === "string" && !items.has(itemAny.name)) {
2328
- items.set(itemAny.name, item);
2329
- }
2330
- }
2381
+ await this.admitLoaderItems(loader, type, items);
2331
2382
  this.reportLoaderReadRecovered(loader.contract.name);
2332
2383
  } catch (e) {
2333
2384
  degraded = true;
@@ -2480,13 +2531,7 @@ var _MetadataManager = class _MetadataManager {
2480
2531
  }
2481
2532
  }
2482
2533
  for (const loader of this.loaders.values()) {
2483
- const loaderItems = await loader.loadMany(type);
2484
- for (const item of loaderItems) {
2485
- const itemAny = item;
2486
- if (itemAny && typeof itemAny.name === "string" && !items.has(itemAny.name)) {
2487
- items.set(itemAny.name, item);
2488
- }
2489
- }
2534
+ await this.admitLoaderItems(loader, type, items);
2490
2535
  }
2491
2536
  return Array.from(items.values());
2492
2537
  }
@@ -2731,12 +2776,54 @@ var _MetadataManager = class _MetadataManager {
2731
2776
  * Runtime-authored `shared` / `personal` views (`sys_view_definition`) are
2732
2777
  * merged in by the REST layer; this method returns the `package` layer that
2733
2778
  * was registered from source.
2779
+ *
2780
+ * ## [#13913] Aggregated containers are expanded inline, per read
2781
+ *
2782
+ * `this.list('view')` is `MetadataManager`'s OWN loader-based store — the
2783
+ * in-memory registry plus every registered loader — and is a completely
2784
+ * different store from the `sys_metadata` rows `getMetaItems` reads. #13407
2785
+ * taught `getMetaItems` to expand a runtime-authored aggregated container
2786
+ * inline; this exit never called it and had no equivalent step, so a
2787
+ * container that `GET /meta/view?object=` now serves still answered **empty**
2788
+ * here.
2789
+ *
2790
+ * Merely getting the container into the store would not have helped: the
2791
+ * filter also requires `viewKind`, and a container has none. Loosening that
2792
+ * requirement is NOT the repair — it would answer with the container itself
2793
+ * as a view, the behaviour #7163 ruled wrong — so what is added below is the
2794
+ * container's **expansion**, whose items each carry the `viewKind` + `object`
2795
+ * pair this filter has always tested. The filter itself is untouched: it
2796
+ * reads the top-level `object`, exactly as `ViewSchema.object` declares.
2797
+ *
2798
+ * Registry-free and per-read, mirroring #13407's choice at the other exit and
2799
+ * for the same reason — the registry is process-wide, so a read must not
2800
+ * graft rows into it (see `view-container-expansion.ts`'s header, which also
2801
+ * records why the protocol's copy of this logic cannot be imported).
2802
+ *
2803
+ * Already-present items win: an expansion contributes only names the store
2804
+ * does not already hold, so a container whose expanded ViewItems were
2805
+ * registered by a source registrar (the ObjectQL boot loop, the artifact/HMR
2806
+ * loader) still answers with those registered, fully-enriched items and this
2807
+ * step adds nothing.
2734
2808
  */
2735
2809
  async getViewsByObject(object) {
2736
2810
  const views = await this.list("view");
2737
- return views.filter(
2811
+ const matches = views.filter(
2738
2812
  (v) => v && typeof v === "object" && v.viewKind && v.object === object
2739
- ).sort(
2813
+ );
2814
+ const known = /* @__PURE__ */ new Set();
2815
+ for (const v of views) {
2816
+ if (v && typeof v === "object" && typeof v.name === "string") known.add(v.name);
2817
+ }
2818
+ for (const v of views) {
2819
+ for (const item of expandRuntimeViewContainer(v)) {
2820
+ if (!item.viewKind || item.object !== object) continue;
2821
+ if (known.has(item.name)) continue;
2822
+ known.add(item.name);
2823
+ matches.push(item);
2824
+ }
2825
+ }
2826
+ return matches.sort(
2740
2827
  (a, b) => (a.order ?? 0) - (b.order ?? 0) || String(a.name).localeCompare(String(b.name))
2741
2828
  );
2742
2829
  }
@@ -3154,66 +3241,18 @@ var _MetadataManager = class _MetadataManager {
3154
3241
  };
3155
3242
  }
3156
3243
  // ==========================================
3157
- // Overlay / Customization Management
3244
+ // Overlay / Customization Management — REMOVED (#13135, ADR-0049)
3158
3245
  // ==========================================
3159
- overlayKey(type, name, scope = "platform") {
3160
- return `${encodeURIComponent(type)}:${encodeURIComponent(name)}:${scope}`;
3161
- }
3162
- /**
3163
- * Get the active overlay for a metadata item
3164
- */
3165
- async getOverlay(type, name, scope) {
3166
- return this.overlays.get(this.overlayKey(type, name, scope ?? "platform"));
3167
- }
3168
- /**
3169
- * Save/update an overlay for a metadata item
3170
- */
3171
- async saveOverlay(overlay) {
3172
- if (this.config.persistence?.overlayWritable === false) {
3173
- const msg = `MetadataManager overlays are read-only (persistence.overlayWritable=false); refusing to save overlay for ${overlay.baseType}/${overlay.baseName}`;
3174
- if (this.config.validation?.throwOnError) {
3175
- throw new Error(msg);
3176
- }
3177
- this.logger.warn(msg);
3178
- return;
3179
- }
3180
- const key = this.overlayKey(overlay.baseType, overlay.baseName, overlay.scope);
3181
- this.overlays.set(key, overlay);
3182
- }
3183
- /**
3184
- * Remove an overlay, reverting to the base definition
3185
- */
3186
- async removeOverlay(type, name, scope) {
3187
- this.overlays.delete(this.overlayKey(type, name, scope ?? "platform"));
3188
- }
3189
- /**
3190
- * Get the effective (merged) metadata after applying all overlays.
3191
- * Resolution order: system ← merge(platform) ← merge(user)
3192
- */
3193
- async getEffective(type, name, context) {
3194
- const base = await this.get(type, name);
3195
- if (!base) return void 0;
3196
- let effective = { ...base };
3197
- const platformOverlay = await this.getOverlay(type, name, "platform");
3198
- if (platformOverlay?.active && platformOverlay.patch) {
3199
- effective = { ...effective, ...platformOverlay.patch };
3200
- }
3201
- if (context?.userId) {
3202
- const userOverlayKey = this.overlayKey(type, name, "user") + `:${context.userId}`;
3203
- const userOverlay = this.overlays.get(userOverlayKey) ?? await this.getOverlay(type, name, "user");
3204
- if (userOverlay?.active && userOverlay.patch) {
3205
- if (!userOverlay.owner || userOverlay.owner === context.userId) {
3206
- effective = { ...effective, ...userOverlay.patch };
3207
- }
3208
- }
3209
- } else {
3210
- const userOverlay = await this.getOverlay(type, name, "user");
3211
- if (userOverlay?.active && userOverlay.patch && !userOverlay.owner) {
3212
- effective = { ...effective, ...userOverlay.patch };
3213
- }
3214
- }
3215
- return effective;
3216
- }
3246
+ //
3247
+ // The in-memory overlay limb (`getOverlay` / `saveOverlay` / `removeOverlay`
3248
+ // / `getEffective`, keyed `type:name:scope`) implemented the paper
3249
+ // metadata-customization protocol removed from `@objectstack/spec` in the
3250
+ // same change: no route ever served the paper `.../overlay` or
3251
+ // `.../effective` endpoints, and the only callers of these methods were this
3252
+ // package's own unit tests. ADR-0126 supersedes the protocol on the record.
3253
+ // The org-scoped customization that actually ships is ADR-0005's
3254
+ // `sys_metadata` overlay (`getMetaItemLayered` in metadata-protocol), which
3255
+ // never lived here.
3217
3256
  // ==========================================
3218
3257
  // Watch / Subscribe (IMetadataService)
3219
3258
  // ==========================================
@@ -3987,6 +4026,7 @@ var MetadataManager = _MetadataManager;
3987
4026
  // src/plugin.ts
3988
4027
  import { readFile as readFile2 } from "fs/promises";
3989
4028
  import { createHash as createHash2 } from "crypto";
4029
+ import { resolveArtifactPackageOrder, artifactPackageId } from "@objectstack/core";
3990
4030
 
3991
4031
  // src/node-metadata-manager.ts
3992
4032
  import * as path2 from "path";
@@ -3997,7 +4037,11 @@ import * as fs from "fs/promises";
3997
4037
  import * as path from "path";
3998
4038
  import { glob } from "glob";
3999
4039
  import { createHash } from "crypto";
4000
- var FilesystemLoader = class {
4040
+ function ownNameOf(data) {
4041
+ const own = data?.name;
4042
+ return typeof own === "string" && own !== "" ? own : null;
4043
+ }
4044
+ var _FilesystemLoader = class _FilesystemLoader {
4001
4045
  constructor(rootDir, serializers, logger) {
4002
4046
  this.rootDir = rootDir;
4003
4047
  this.serializers = serializers;
@@ -4095,6 +4139,82 @@ var FilesystemLoader = class {
4095
4139
  }
4096
4140
  }
4097
4141
  async loadMany(type, options) {
4142
+ return (await this.loadManyEntries(type, options)).map((entry) => entry.data);
4143
+ }
4144
+ /**
4145
+ * [#14341] The keyed half of {@link loadMany} — see {@link MetadataKeyedItem}
4146
+ * for why the store's key travels BESIDE the body instead of being folded
4147
+ * into it.
4148
+ *
4149
+ * THE RULE, in one sentence: an item is keyed by this loader's own
4150
+ * name-to-path derivation — {@link nameFromFilename}, the very basename
4151
+ * derivation `list()` reports — ONLY where that derivation is a bijection for
4152
+ * the file (it sits directly under `ROOT/TYPE/` and carries one of the
4153
+ * extensions {@link findFile} tries, so `findFile(type, key)` resolves back to
4154
+ * this same file); every other shape keeps the pre-#14205 behaviour verbatim,
4155
+ * keyed by `body.name` when it has one and dropped when it has none.
4156
+ *
4157
+ * Why the rule stops there (PM ruling on #14341, 2026-09-02, knowingly over
4158
+ * triage's "a nested path keeps whatever `list()` reports for it today"):
4159
+ * `list()` and `findFile()` DISAGREE outside that shape. For
4160
+ * `ROOT/TYPE/crm/account.json`, `list()` reports the bare `account`, but
4161
+ * `findFile()` resolves that name against `ROOT/TYPE/account.json` and finds
4162
+ * nothing — the only name reaching the file is `crm/account`, which nothing
4163
+ * reports. An extension-less file is read by `loadMany()` and reported by
4164
+ * `list()`, and `findFile()` resolves neither. Keying by either side would
4165
+ * mint a name some other door cannot open, and two directories holding the
4166
+ * same basename would collide in silence
4167
+ * (`MetadataManager.admitLoaderItems()` keeps the first and says nothing).
4168
+ * The card's own fence: "keying items under names nothing else uses … is
4169
+ * worse than today's honest drop". So the drop stays exactly where the key is
4170
+ * unsettled, and is pinned as a RECORD in
4171
+ * `filesystem-loader-keyed-items.test.ts`.
4172
+ *
4173
+ * [#14486, partial] `list()` and {@link findFile} have since converged on
4174
+ * {@link resolvableNameForPath} — the derivation this method already used —
4175
+ * so a nested or extension-less file is now neither listed nor resolvable.
4176
+ * What did NOT change is the WALK behind this method: `loadManyEntries()`
4177
+ * still READS those files, so `loadMany()` still returns their bodies and
4178
+ * this method still falls back to `body.name` for them. That half of the
4179
+ * #14486 ruling ("nothing unlisted is returned by `loadMany()` either") is
4180
+ * deliberately NOT taken here: it would invert the three landed #14341 pins
4181
+ * in `filesystem-loader-keyed-items.test.ts:113,167,187` and the
4182
+ * `loadMany()` CONTROL at `:196`, and that file was under a concurrent
4183
+ * claim (PR #14627) when this landed. The remaining divergence — listed ⊂
4184
+ * loaded — is pinned as a RECORD in
4185
+ * `filesystem-loader-list-reachability.test.ts` rather than left implicit.
4186
+ *
4187
+ * One consequence, deliberate: a flat file whose `body.name` DISAGREES with
4188
+ * its basename is now keyed by the BASENAME. That is #14205's rule (identity
4189
+ * is the key the store holds an item under, not `body.name`) applied to this
4190
+ * loader, and it aligns `MetadataManager.list()` with `listNames()` for that
4191
+ * shape.
4192
+ *
4193
+ * The body is handed back by reference, unchanged: nothing is written into a
4194
+ * body that deliberately has no `name`. `limit` bounds the items LOADED,
4195
+ * exactly as `loadMany()` does — an entry the key rule drops has still been
4196
+ * read and still counts against it.
4197
+ */
4198
+ async loadManyKeyed(type, options) {
4199
+ const typeDir = path.join(this.rootDir, type);
4200
+ const keyed = [];
4201
+ for (const entry of await this.loadManyEntries(type, options)) {
4202
+ const name = this.resolvableNameForPath(typeDir, entry.file) ?? ownNameOf(entry.data);
4203
+ if (name) {
4204
+ keyed.push({ name, data: entry.data });
4205
+ }
4206
+ }
4207
+ return keyed;
4208
+ }
4209
+ /**
4210
+ * The single walk behind {@link loadMany} and {@link loadManyKeyed}: one glob,
4211
+ * one serializer pass, one `limit`. Shared so the two can never answer with
4212
+ * different bodies for the same file — {@link MetadataLoader.loadManyKeyed}
4213
+ * requires `data` to be "the same body `loadMany()` would return for the
4214
+ * item", and a second copy of this walk is how that would quietly stop being
4215
+ * true.
4216
+ */
4217
+ async loadManyEntries(type, options) {
4098
4218
  const { patterns = ["**/*"], recursive: _recursive = true, limit } = options || {};
4099
4219
  const typeDir = path.join(this.rootDir, type);
4100
4220
  const items = [];
@@ -4117,7 +4237,7 @@ var FilesystemLoader = class {
4117
4237
  const serializer = this.getSerializer(format);
4118
4238
  if (serializer) {
4119
4239
  const data = serializer.deserialize(content);
4120
- items.push(data);
4240
+ items.push({ file, data });
4121
4241
  }
4122
4242
  } catch (error) {
4123
4243
  this.logger?.warn("Failed to load file", {
@@ -4171,6 +4291,30 @@ var FilesystemLoader = class {
4171
4291
  return null;
4172
4292
  }
4173
4293
  }
4294
+ /**
4295
+ * [#14486] The names this loader can be asked for, and ONLY those: a file
4296
+ * directly under `ROOT/TYPE/` carrying an extension one of this instance's
4297
+ * REGISTERED serializers claims. Every name it reports resolves back through
4298
+ * {@link findFile}, so `listNames()` and `get()` give the same answer.
4299
+ *
4300
+ * It used to report `path.basename(file, ext)` for every file the glob found,
4301
+ * nested or not, extension or not — and {@link findFile} resolves neither
4302
+ * shape. `ROOT/TYPE/crm/account.json` was listed as `account`, which resolves
4303
+ * against `ROOT/TYPE/account.json` and finds nothing; an extension-less
4304
+ * `ROOT/TYPE/noext` was listed as `noext`, which resolves under no appended
4305
+ * extension at all. A name in the list that `get()` answers `null` for is the
4306
+ * silent failure an author (human or AI) reads as their own typo, so they
4307
+ * retry the same word: the list and the door now agree instead.
4308
+ *
4309
+ * Ruling (maintainer, via the director seat on #14486, 2026-09-02): narrow
4310
+ * the list — direction A, over B (reverse-unify: report `crm/account` and
4311
+ * teach `findFile()` path-shaped names), which would have made a slash inside
4312
+ * a metadata name every consumer's permanent obligation with no measured
4313
+ * demand for it. The two-segment layout follows ADR-0008 §10, which
4314
+ * `metadata-fs`'s `parseItemPath()` already enforces for its own store; the
4315
+ * EXTENSION set deliberately does NOT follow §10's `.json`-only rule — see
4316
+ * {@link resolvableExtensions} for why.
4317
+ */
4174
4318
  async list(type) {
4175
4319
  const typeDir = path.join(this.rootDir, type);
4176
4320
  try {
@@ -4179,11 +4323,7 @@ var FilesystemLoader = class {
4179
4323
  ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*"],
4180
4324
  nodir: true
4181
4325
  });
4182
- return files.map((file) => {
4183
- const ext = path.extname(file);
4184
- const basename3 = path.basename(file, ext);
4185
- return basename3;
4186
- });
4326
+ return files.map((file) => this.resolvableNameForPath(typeDir, path.join(typeDir, file))).filter((name) => name !== null);
4187
4327
  } catch (error) {
4188
4328
  this.logger?.error("Failed to list", void 0, {
4189
4329
  type,
@@ -4261,12 +4401,66 @@ var FilesystemLoader = class {
4261
4401
  throw error;
4262
4402
  }
4263
4403
  }
4404
+ /**
4405
+ * [#14486] The extensions a name can be resolved under, for THIS instance:
4406
+ * the ones belonging to the serializer set it was constructed with. Shared by
4407
+ * {@link findFile}, {@link resolvableNameForPath} and therefore {@link list},
4408
+ * so the set a name can be RESOLVED under cannot drift from the set that is
4409
+ * LISTED or the set {@link loadManyKeyed} is willing to KEY by.
4410
+ *
4411
+ * Registered, not hard-coded, and deliberately not ADR-0008 §10's `.json`
4412
+ * only. §10 governs the `metadata-fs` store; applying it verbatim here would
4413
+ * drop `.yaml` and `.ts` metadata out of `listNames()` — a breakage this card
4414
+ * never asked for. Under the manager's DEFAULT format set
4415
+ * (`typescript` / `json` / `yaml`, `metadata-manager.ts`) that leaves `.js`
4416
+ * out, which is the card's row-4 membership mismatch closing for free: a `.js`
4417
+ * file was listed and resolvable while `loadMany()` could never return it and
4418
+ * `load()` threw `No serializer found for format: javascript`. Register
4419
+ * `javascript` and it is listed, resolvable and loadable together.
4420
+ */
4421
+ resolvableExtensions() {
4422
+ const extensions = [];
4423
+ for (const [format, formatExtensions] of _FilesystemLoader.EXTENSIONS_BY_FORMAT) {
4424
+ if (this.serializers.has(format)) {
4425
+ extensions.push(...formatExtensions);
4426
+ }
4427
+ }
4428
+ return extensions;
4429
+ }
4430
+ /**
4431
+ * The metadata name this loader reports for a file: the basename with its
4432
+ * extension stripped. One derivation, shared by {@link list} and
4433
+ * {@link loadManyKeyed}, so the two cannot drift for the shape where they
4434
+ * agree — `dotted.config.json` is `dotted.config` for both.
4435
+ */
4436
+ static nameFromFilename(file) {
4437
+ return path.basename(file, path.extname(file));
4438
+ }
4439
+ /**
4440
+ * The key for a file IF this loader's name-to-path mapping is a bijection for
4441
+ * it: a file directly under `ROOT/TYPE/` carrying an extension
4442
+ * {@link findFile} tries, so `findFile(type, key)` resolves back to this very
4443
+ * file. `null` for every other shape — a nested path, an extension-less file,
4444
+ * an extension spelled in a case `findFile()` does not compose — which is why
4445
+ * {@link loadManyKeyed} falls back to `body.name` there rather than minting a
4446
+ * key no other door can open.
4447
+ */
4448
+ resolvableNameForPath(typeDir, file) {
4449
+ const rel = path.relative(typeDir, file);
4450
+ if (rel === "" || rel.split(path.sep).length !== 1) {
4451
+ return null;
4452
+ }
4453
+ if (!this.resolvableExtensions().includes(path.extname(rel))) {
4454
+ return null;
4455
+ }
4456
+ return _FilesystemLoader.nameFromFilename(rel);
4457
+ }
4264
4458
  /**
4265
4459
  * Find file for a given type and name
4266
4460
  */
4267
4461
  async findFile(type, name) {
4268
4462
  const typeDir = path.join(this.rootDir, type);
4269
- const extensions = [".json", ".yaml", ".yml", ".ts", ".js"];
4463
+ const extensions = this.resolvableExtensions();
4270
4464
  for (const ext of extensions) {
4271
4465
  const filePath = path.join(typeDir, `${name}${ext}`);
4272
4466
  try {
@@ -4312,6 +4506,19 @@ var FilesystemLoader = class {
4312
4506
  return `"${hash}"`;
4313
4507
  }
4314
4508
  };
4509
+ /**
4510
+ * The inverse of {@link detectFormat}: which file extensions carry which
4511
+ * format. Fixed ORDER, because it is also {@link findFile}'s precedence when
4512
+ * two files under one type directory share a stem — registration order must
4513
+ * not be able to change which file `ROOT/TYPE/NAME` opens.
4514
+ */
4515
+ _FilesystemLoader.EXTENSIONS_BY_FORMAT = [
4516
+ ["json", [".json"]],
4517
+ ["yaml", [".yaml", ".yml"]],
4518
+ ["typescript", [".ts"]],
4519
+ ["javascript", [".js"]]
4520
+ ];
4521
+ var FilesystemLoader = _FilesystemLoader;
4315
4522
 
4316
4523
  // src/node-metadata-manager.ts
4317
4524
  var NodeMetadataManager = class extends MetadataManager {
@@ -4436,6 +4643,20 @@ var MemoryLoader = class {
4436
4643
  if (!typeStore) return [];
4437
4644
  return Array.from(typeStore.values());
4438
4645
  }
4646
+ /**
4647
+ * [#14205] The keyed half of {@link loadMany}. The storage map is already
4648
+ * `Type -> Name -> Data`, so the key this loader holds an item under is the
4649
+ * map key — `loadMany()` was simply discarding it, which dropped every
4650
+ * nameless body out of `MetadataManager.list()` and out of the endpoint index.
4651
+ *
4652
+ * The body is handed back by reference, unchanged: the key travels beside it,
4653
+ * never folded into it.
4654
+ */
4655
+ async loadManyKeyed(type, _options) {
4656
+ const typeStore = this.storage.get(type);
4657
+ if (!typeStore) return [];
4658
+ return Array.from(typeStore, ([name, data]) => ({ name, data }));
4659
+ }
4439
4660
  async exists(type, name) {
4440
4661
  return this.storage.get(type)?.has(name) ?? false;
4441
4662
  }
@@ -4482,19 +4703,23 @@ var MemoryLoader = class {
4482
4703
 
4483
4704
  // src/plugin.ts
4484
4705
  import { DEFAULT_METADATA_TYPE_REGISTRY } from "@objectstack/spec/kernel";
4485
- import { applyProtection } from "@objectstack/spec/shared";
4706
+ import { applyProtection as applyProtection2 } from "@objectstack/spec/shared";
4486
4707
  import {
4487
- SysMetadataObject as SysMetadataObject2,
4488
- SysMetadataHistoryObject as SysMetadataHistoryObject2,
4708
+ SysMetadataObject as SysMetadataObject3,
4709
+ SysMetadataHistoryObject as SysMetadataHistoryObject3,
4489
4710
  SysMetadataCommitObject,
4490
4711
  SysMetadataAuditObject,
4491
- SysViewDefinitionObject
4712
+ SysViewDefinitionObject,
4713
+ applyArtifactForwardConversions,
4714
+ detectUnboundFormViewPredicateRoots,
4715
+ BOUND_FORM_VIEW_PREDICATE_ROOTS,
4716
+ BOUND_FORM_FIELD_PREDICATE_ROOTS
4492
4717
  } from "@objectstack/metadata-core";
4493
- import { isAggregatedViewContainer, expandViewContainer } from "@objectstack/spec";
4494
4718
  import { isAggregatedViewContainer as isAggregatedViewContainer2, expandViewContainer as expandViewContainer2 } from "@objectstack/spec";
4719
+ import { isAggregatedViewContainer as isAggregatedViewContainer3, expandViewContainer as expandViewContainer3 } from "@objectstack/spec";
4495
4720
  var queryableMetadataObjects = [
4496
- SysMetadataObject2,
4497
- SysMetadataHistoryObject2,
4721
+ SysMetadataObject3,
4722
+ SysMetadataHistoryObject3,
4498
4723
  // ADR-0067 commit log — sibling of sys_metadata_history (see note above).
4499
4724
  SysMetadataCommitObject,
4500
4725
  SysMetadataAuditObject,
@@ -4524,8 +4749,37 @@ var ARTIFACT_FIELD_TO_TYPE = {
4524
4749
  // positions from artifact ingestion.
4525
4750
  positions: "position",
4526
4751
  permissions: "permission",
4752
+ // [ADR-0066 D1] `capabilities` reaches the door at #12892 step 1, the
4753
+ // maintainer's `option 1` ruling ("the door owns the registration
4754
+ // route" for the five artifact security collections). Until #12894
4755
+ // measured it, `AppPlugin`'s `SECURITY_FIELDS` block
4756
+ // (packages/runtime/src/app-plugin.ts) was this collection's SOLE
4757
+ // registrar on an artifact boot — the one security collection the door
4758
+ // could not reach — so a declared capability was registered from bytes
4759
+ // nothing strict-parses, with no schema default and no ADR-0010
4760
+ // provenance. Measured on the two-reader harness, the door's copy adds
4761
+ // exactly four keys the raw copy lacks: `scope` (the schema default)
4762
+ // and `_packageId` / `_packageVersion` / `_provenance`.
4763
+ //
4764
+ // ⚠️ This entry makes the door a SECOND writer, not yet the only one:
4765
+ // `AppPlugin` still registers `capabilities`, and it runs last, so the
4766
+ // raw copy still wins a real artifact boot. Step 2 of the ruling (that
4767
+ // block stops registering these five on the artifact path, after a
4768
+ // census of the non-artifact boot paths) is what makes this the only
4769
+ // copy. Until then the divergence is the interim reality the ruling
4770
+ // explicitly permits, and #12878's pins are what keep it visible.
4771
+ capabilities: "capability",
4527
4772
  sharingRules: "sharing_rule",
4528
- policies: "policy",
4773
+ // `policies: 'policy'` removed at #12894: the stack schema is a
4774
+ // `strictObject` that declares no top-level `policies` key, so a
4775
+ // definition carrying one is refused by the strict parse a few lines
4776
+ // below — the entry could never match, and nothing was ever registered
4777
+ // under `policy` from this map. The word is real, but it lives ONE LEVEL
4778
+ // DOWN: on a permission set it is an alias for `rowLevelSecurity`
4779
+ // (`PERMISSION_SET_KEY_ALIASES`, packages/spec/src/security/permission.zod.ts)
4780
+ // — a key on an ITEM, never a collection. Third retirement of this exact
4781
+ // shape in this map (`themes` and `roles` above); the reasons are kept
4782
+ // in place because the first two are what made this one findable.
4529
4783
  apis: "api",
4530
4784
  webhooks: "webhook",
4531
4785
  agents: "agent",
@@ -4568,6 +4822,21 @@ var MetadataPlugin = class {
4568
4822
  * degrades on purpose (objects are discovered via the legacy fallback).
4569
4823
  */
4570
4824
  this.optionalDependencies = ["com.objectstack.engine.objectql"];
4825
+ /**
4826
+ * Once-per-process dedupe for the summaries the versioned artifact window
4827
+ * emits. The artifact watcher replays `_parseAndRegisterArtifact` on every
4828
+ * file change, so without this a dev loop over a legacy artifact would
4829
+ * re-announce the same finding on every reload — the same shape
4830
+ * `Protocol.storedConversionWarned` guards on the stored-row pass, which
4831
+ * this surfacing is modeled on.
4832
+ *
4833
+ * Two key families share the set, because they share the replay:
4834
+ * `<conversionId>|<label>` for a forward-conversion summary (#12772), and
4835
+ * `unbound-form-predicate-root|<label>` for the unbound-root notice
4836
+ * (#12915) — one line per artifact there, not one per conversion, since
4837
+ * the notice already aggregates every finding it made.
4838
+ */
4839
+ this.artifactConversionWarned = /* @__PURE__ */ new Set();
4571
4840
  this.init = async (ctx) => {
4572
4841
  this.initCtx = ctx;
4573
4842
  ctx.logger.info("Initializing Metadata Manager", {
@@ -4576,7 +4845,6 @@ var MetadataPlugin = class {
4576
4845
  artifactSource: this.options.artifactSource?.mode
4577
4846
  });
4578
4847
  ctx.registerService("metadata", this.manager);
4579
- console.log("[MetadataPlugin] Registered metadata service, has getRegisteredTypes:", typeof this.manager.getRegisteredTypes);
4580
4848
  const registerSysObjects = this.options.registerSystemObjects !== false;
4581
4849
  if (registerSysObjects) {
4582
4850
  try {
@@ -4598,7 +4866,7 @@ var MetadataPlugin = class {
4598
4866
  }
4599
4867
  ctx.logger.info("MetadataPlugin providing metadata service (primary mode)", {
4600
4868
  mode: this.options.artifactSource?.mode ?? "file-system",
4601
- features: ["watch", "multi-format", "query", "overlay", "type-registry"]
4869
+ features: ["watch", "multi-format", "query", "type-registry"]
4602
4870
  });
4603
4871
  };
4604
4872
  this.start = async (ctx) => {
@@ -4681,7 +4949,7 @@ var MetadataPlugin = class {
4681
4949
  if (httpServer && typeof httpServer.getRawApp === "function") {
4682
4950
  const { registerMetadataHmrRoutes: registerMetadataHmrRoutes2 } = await Promise.resolve().then(() => (init_hmr_routes(), hmr_routes_exports));
4683
4951
  const hub = registerMetadataHmrRoutes2(httpServer.getRawApp(), this.manager);
4684
- hub.setOnPostReload(async (body = {}) => {
4952
+ hub?.setOnPostReload(async (body = {}) => {
4685
4953
  const src3 = this.options.artifactSource;
4686
4954
  if (src3?.mode === "local-file") {
4687
4955
  try {
@@ -4721,7 +4989,7 @@ var MetadataPlugin = class {
4721
4989
  pending = true;
4722
4990
  try {
4723
4991
  await this._reloadAndAnnounce(ctx, src2, [src2.path]);
4724
- hub.broadcastReload("artifact-file-changed", [src2.path]);
4992
+ hub?.broadcastReload("artifact-file-changed", [src2.path]);
4725
4993
  ctx.logger.info("[MetadataPlugin] artifact auto-reloaded (file watcher)", {
4726
4994
  path: src2.path
4727
4995
  });
@@ -4743,7 +5011,13 @@ var MetadataPlugin = class {
4743
5011
  ctx.logger.warn("[MetadataPlugin] artifact watcher failed to start", { error: e?.message });
4744
5012
  }
4745
5013
  }
4746
- console.log("[MetadataPlugin] HMR endpoint registered at /api/v1/dev/metadata-events");
5014
+ if (hub) {
5015
+ console.log("[MetadataPlugin] HMR endpoint registered at /api/v1/dev/metadata-events");
5016
+ } else {
5017
+ console.log(
5018
+ `[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"})`
5019
+ );
5020
+ }
4747
5021
  } else {
4748
5022
  console.log("[MetadataPlugin] HTTP server with getRawApp() not available \u2014 skipping HMR endpoint");
4749
5023
  }
@@ -4839,6 +5113,106 @@ var MetadataPlugin = class {
4839
5113
  if (timer) clearTimeout(timer);
4840
5114
  }
4841
5115
  }
5116
+ /**
5117
+ * Versioned ADR-0087 forward conversion at the artifact-ingestion door
5118
+ * (#12772) — runs BEFORE the strict schema parse below, because the parse
5119
+ * is the refusal point.
5120
+ *
5121
+ * A compiled artifact is data at rest with a version stamp: built by
5122
+ * released tooling, then unchanged while the platform moves on. When a
5123
+ * spec release retires an authorable key inside a protocol line (spec
5124
+ * 17.1 → 17.2 retired the `allowRestore`/`allowPurge` permission bits),
5125
+ * every already-built artifact carrying the key becomes unbootable at the
5126
+ * tombstone — with no operator remedy, since `os migrate meta` targets
5127
+ * sources, not built artifacts. The stored-row read path already replays
5128
+ * the conversion chain for exactly this reason
5129
+ * (`applyConversionsToStoredItem`, ADR-0087 addendum); this is the same
5130
+ * policy at the artifact door, **keyed off the artifact's own declared
5131
+ * `engines.protocol` floor**: an artifact authored below the running spec
5132
+ * version converts forward, an artifact authored at the current (or a
5133
+ * newer) surface converts nothing and answers to the strict parse,
5134
+ * tombstones included. The version key is what keeps this a conversion
5135
+ * rather than an amnesty — the retired keys return with the M2 lifecycle
5136
+ * batch (#1883), and artifacts authored against that surface must never
5137
+ * have them stripped by history.
5138
+ *
5139
+ * Notices surface the way the stored-row pass's do — operator-visible and
5140
+ * deduped — as one summary line per conversion per artifact rather than
5141
+ * one per rewritten path (a real 17.1 artifact carried 150 strips of the
5142
+ * same two keys; 150 identical warn lines would bury the boot log).
5143
+ */
5144
+ _convertArtifactForward(ctx, definition, label) {
5145
+ const result = applyArtifactForwardConversions(definition);
5146
+ this._warnUnboundFormPredicateRoots(ctx, result, label);
5147
+ if (result.notices.length === 0) return result.definition;
5148
+ const byConversion = /* @__PURE__ */ new Map();
5149
+ for (const n of result.notices) {
5150
+ const existing = byConversion.get(n.conversionId);
5151
+ if (existing) existing.count += 1;
5152
+ else byConversion.set(n.conversionId, { count: 1, firstPath: n.path, message: n.message });
5153
+ }
5154
+ for (const [conversionId, agg] of byConversion) {
5155
+ const key = `${conversionId}|${label}`;
5156
+ if (this.artifactConversionWarned.has(key)) continue;
5157
+ this.artifactConversionWarned.add(key);
5158
+ ctx.logger.warn(
5159
+ `[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.`
5160
+ );
5161
+ }
5162
+ return result.definition;
5163
+ }
5164
+ /**
5165
+ * Operator-facing boot notice for form-view predicates that fault OPEN on
5166
+ * this runtime (#12915 scope C — maintainer ruling 2026-08-28, 「同意C」).
5167
+ *
5168
+ * A form-view predicate binds `record` / `previous` / `parent` (runtime
5169
+ * record forms) or `data` (metadata-editing forms) — and a FIELD-level one
5170
+ * also binds `current_user` and its ADR-0068 aliases (objectui#6010),
5171
+ * which a SECTION-level one does not. The contract states beside that
5172
+ * vocabulary that a bare identifier is UNBOUND and the predicate faults,
5173
+ * and `visibleWhen`'s fault fallback is `true`. On a real
5174
+ * 17.1-built artifact that combination dead-ends record creation in the
5175
+ * console: the conditionally hidden field renders, and its unconditional
5176
+ * `required: true` — authored to be gated by the visibility that no longer
5177
+ * applies — blocks every submit, while the same payload POSTs 201 through
5178
+ * REST. Nothing refused, nothing logged, and only the operator can fix it
5179
+ * (by rebuilding the artifact), so this is the channel the ruling picked:
5180
+ * service startup, server-side, never a console surface — the person at
5181
+ * the form cannot act on "your artifact is stale".
5182
+ *
5183
+ * **Detection only.** No refusal, no rewrite, no behaviour change: the
5184
+ * predicate keeps faulting open exactly as before. Rewriting a bare root to
5185
+ * `record.` is the ADR-0087 conversion (#12915 scope A), deferred by the
5186
+ * same ruling with an explicit start line.
5187
+ *
5188
+ * **Same versioned window as the conversion replay above** — and read off
5189
+ * that pass's own verdict rather than recomputed, so the two can never
5190
+ * disagree about which artifacts are "old". An artifact declaring the
5191
+ * current (or a newer) floor answers to the strict parse and gets nothing
5192
+ * from here even when it does carry bare roots; that boundary is what keeps
5193
+ * a notice about legacy artifacts out of contract territory. An undeclared
5194
+ * range is treated as old data at rest, matching the grandfathering posture
5195
+ * the window already takes (`converted-undeclared`).
5196
+ */
5197
+ _warnUnboundFormPredicateRoots(ctx, result, label) {
5198
+ if (result.verdict !== "converted-forward" && result.verdict !== "converted-undeclared") return;
5199
+ const findings = detectUnboundFormViewPredicateRoots(result.definition);
5200
+ if (findings.length === 0) return;
5201
+ const key = `unbound-form-predicate-root|${label}`;
5202
+ if (this.artifactConversionWarned.has(key)) return;
5203
+ this.artifactConversionWarned.add(key);
5204
+ const views = [...new Set(findings.map((f) => f.view))];
5205
+ const roots = [...new Set(findings.map((f) => f.root))];
5206
+ const quote = (list) => list.map((v) => `'${v}'`).join(", ");
5207
+ const surfaces = new Set(findings.map((f) => f.surface));
5208
+ const vocabulary = [
5209
+ surfaces.has("field") ? `on a form FIELD: ${quote(BOUND_FORM_FIELD_PREDICATE_ROOTS)}` : null,
5210
+ surfaces.has("section") ? `on a form SECTION: ${quote(BOUND_FORM_VIEW_PREDICATE_ROOTS)}` : null
5211
+ ].filter(Boolean).join("; ");
5212
+ ctx.logger.warn(
5213
+ `[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>').`
5214
+ );
5215
+ }
4842
5216
  /**
4843
5217
  * Parse raw artifact JSON (envelope or bare definition) and register all
4844
5218
  * metadata items into the MetadataManager.
@@ -4857,16 +5231,22 @@ var MetadataPlugin = class {
4857
5231
  let metadata;
4858
5232
  const obj = raw;
4859
5233
  if (obj?.schemaVersion && obj?.commitId && obj?.metadata !== void 0) {
4860
- const artifact = EnvironmentArtifactSchema.parse(obj);
5234
+ const artifact = EnvironmentArtifactSchema.parse({
5235
+ ...obj,
5236
+ metadata: this._convertArtifactForward(ctx, obj.metadata, label)
5237
+ });
4861
5238
  metadata = artifact.metadata;
4862
5239
  } else if (obj?.success && obj?.data?.metadata) {
4863
- const artifact = EnvironmentArtifactSchema.parse(obj.data);
5240
+ const artifact = EnvironmentArtifactSchema.parse({
5241
+ ...obj.data,
5242
+ metadata: this._convertArtifactForward(ctx, obj.data.metadata, label)
5243
+ });
4864
5244
  metadata = artifact.metadata;
4865
5245
  } else {
4866
- const def = ObjectStackDefinitionSchema.parse(obj);
5246
+ const def = ObjectStackDefinitionSchema.parse(this._convertArtifactForward(ctx, obj, label));
4867
5247
  const canonical = JSON.stringify(def, Object.keys(def).sort());
4868
5248
  const checksum = createHash2("sha256").update(canonical).digest("hex");
4869
- const environmentId = this.options.environmentId ?? "proj_local";
5249
+ const environmentId = this.options.environmentId ?? "env_local";
4870
5250
  EnvironmentArtifactSchema.parse({
4871
5251
  schemaVersion: "0.1",
4872
5252
  environmentId,
@@ -4880,53 +5260,127 @@ var MetadataPlugin = class {
4880
5260
  const memLoader = new MemoryLoader();
4881
5261
  const manifestPackageId = metadata?.manifest?.id ?? metadata?.id ?? void 0;
4882
5262
  const manifestVersion = metadata?.manifest?.version ?? metadata?.version ?? void 0;
5263
+ const carriesPackages = Array.isArray(metadata?.packages);
5264
+ const bodies = resolveArtifactPackageOrder(metadata);
5265
+ const ownedByPackage = /* @__PURE__ */ new Map();
5266
+ const claim = (type, name) => {
5267
+ let names = ownedByPackage.get(type);
5268
+ if (!names) ownedByPackage.set(type, names = /* @__PURE__ */ new Set());
5269
+ names.add(name);
5270
+ };
5271
+ const claimed = (type, name) => ownedByPackage.get(type)?.has(name) === true;
5272
+ let totalRegistered = 0;
5273
+ for (const body of bodies) {
5274
+ totalRegistered += await this._registerArtifactBodyCollections(
5275
+ ctx,
5276
+ memLoader,
5277
+ body,
5278
+ carriesPackages ? {
5279
+ packageId: artifactPackageId(body),
5280
+ packageVersion: body?.version ?? void 0
5281
+ } : { packageId: manifestPackageId, packageVersion: manifestVersion },
5282
+ { claim: carriesPackages ? claim : void 0 }
5283
+ );
5284
+ }
5285
+ if (carriesPackages) {
5286
+ const residual = await this._registerArtifactBodyCollections(
5287
+ ctx,
5288
+ memLoader,
5289
+ metadata,
5290
+ { packageId: manifestPackageId, packageVersion: manifestVersion },
5291
+ { skip: claimed }
5292
+ );
5293
+ totalRegistered += residual;
5294
+ if (residual > 0) {
5295
+ ctx.logger.warn(
5296
+ `[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.`
5297
+ );
5298
+ }
5299
+ }
5300
+ this.manager.registerLoader(memLoader);
5301
+ ctx.logger.info("[MetadataPlugin] Artifact metadata loaded", { source: label, totalRegistered });
5302
+ return totalRegistered;
5303
+ }
5304
+ /**
5305
+ * Register ONE artifact body's collections into the MetadataManager.
5306
+ *
5307
+ * A "body" is either the whole artifact (the single-package branch, where
5308
+ * the artifact and its one package are the same object) or one entry of
5309
+ * `packages[]` (ADR-0130 D4), which is an assembled
5310
+ * `{ ...manifest, ...collections }` payload carrying the same collection
5311
+ * keys the top level does. The loop is identical for both — that is the
5312
+ * point: there is one ingestion of a collection here, not one per shape.
5313
+ *
5314
+ * @param provenance - The `(packageId, packageVersion)` every item found in
5315
+ * this body is stamped with (ADR-0010 §3.7, via `applyProtection`). It is
5316
+ * the body's OWN identity, never the enclosing artifact's, which is what
5317
+ * makes a multi-package artifact's items agree with the registry and with
5318
+ * `GET /api/v1/packages` about who owns them.
5319
+ * @param slots.claim - Called with every `(type, name)` this pass
5320
+ * registered. Passed when reading package bodies; the residual sweep uses
5321
+ * what it recorded.
5322
+ * @param slots.skip - Consulted before registering each `(type, name)`.
5323
+ * Passed ONLY by the residual sweep, so a package body's copy is never
5324
+ * overwritten by the flattened top-level copy of the same definition —
5325
+ * the overwrite that re-attributed the item to the artifact's manifest.
5326
+ * ⛔ It is never passed while reading the bodies themselves: two items of
5327
+ * one name inside one body still register as they always have (last
5328
+ * wins), because suppressing that would be a behaviour change on the
5329
+ * single-package branch D7 pins.
5330
+ * @returns How many items this body registered.
5331
+ */
5332
+ async _registerArtifactBodyCollections(ctx, memLoader, body, provenance, slots = {}) {
5333
+ const { packageId, packageVersion } = provenance;
4883
5334
  let totalRegistered = 0;
4884
5335
  for (const [field, metaType] of Object.entries(ARTIFACT_FIELD_TO_TYPE)) {
4885
- const items = metadata[field];
5336
+ const items = body[field];
4886
5337
  if (!Array.isArray(items) || items.length === 0) continue;
4887
5338
  for (const item of items) {
4888
- if (metaType === "view" && isAggregatedViewContainer2(item)) {
4889
- const viewObject = item?.list?.data?.object ?? item?.form?.data?.object;
5339
+ if (metaType === "view" && isAggregatedViewContainer3(item)) {
5340
+ const viewObject = deriveViewContainerObject(item);
4890
5341
  if (!viewObject) continue;
4891
- applyProtection(item, {
4892
- packageId: manifestPackageId,
4893
- packageVersion: manifestVersion
5342
+ if (slots.skip?.("view", viewObject)) continue;
5343
+ applyProtection2(item, {
5344
+ packageId,
5345
+ packageVersion
4894
5346
  });
4895
5347
  await memLoader.save("view", viewObject, item);
4896
5348
  await this.manager.register("view", viewObject, item, { notify: false });
4897
5349
  totalRegistered++;
4898
- for (const vi of expandViewContainer2(viewObject, item)) {
5350
+ slots.claim?.("view", viewObject);
5351
+ for (const vi of expandViewContainer3(viewObject, item)) {
4899
5352
  for (const w of vi._diagnostics?.warnings ?? []) {
4900
5353
  ctx.logger.warn(`[MetadataPlugin] View expansion warning for '${vi.name}': ${w.message}`);
4901
5354
  }
4902
- applyProtection(vi, {
4903
- packageId: manifestPackageId,
4904
- packageVersion: manifestVersion
5355
+ applyProtection2(vi, {
5356
+ packageId,
5357
+ packageVersion
4905
5358
  });
4906
5359
  await memLoader.save("view", vi.name, vi);
4907
5360
  await this.manager.register("view", vi.name, vi, { notify: false });
4908
5361
  totalRegistered++;
5362
+ slots.claim?.("view", vi.name);
4909
5363
  }
4910
5364
  continue;
4911
5365
  }
4912
5366
  let name = item?.name;
4913
5367
  if (!name) {
4914
5368
  if (metaType === "view") {
4915
- name = item?.list?.data?.object ?? item?.form?.data?.object;
5369
+ name = deriveViewContainerObject(item);
4916
5370
  }
4917
5371
  }
4918
5372
  if (!name) continue;
4919
- applyProtection(item, {
4920
- packageId: manifestPackageId,
4921
- packageVersion: manifestVersion
5373
+ if (slots.skip?.(metaType, name)) continue;
5374
+ applyProtection2(item, {
5375
+ packageId,
5376
+ packageVersion
4922
5377
  });
4923
5378
  await memLoader.save(metaType, name, item);
4924
5379
  await this.manager.register(metaType, name, item, { notify: false });
4925
5380
  totalRegistered++;
5381
+ slots.claim?.(metaType, name);
4926
5382
  }
4927
5383
  }
4928
- this.manager.registerLoader(memLoader);
4929
- ctx.logger.info("[MetadataPlugin] Artifact metadata loaded", { source: label, totalRegistered });
4930
5384
  return totalRegistered;
4931
5385
  }
4932
5386
  /**
@@ -5002,7 +5456,7 @@ var MetadataPlugin = class {
5002
5456
  for (const item of items) {
5003
5457
  const meta = item;
5004
5458
  if (meta?.name) {
5005
- applyProtection(meta, {
5459
+ applyProtection2(meta, {
5006
5460
  packageId: this.options.packageId
5007
5461
  });
5008
5462
  await this.manager.register(entry.type, meta.name, item, { notify: false });
@@ -5120,7 +5574,7 @@ var RemoteLoader = class {
5120
5574
  };
5121
5575
 
5122
5576
  // src/index.ts
5123
- import { SysMetadataObject as SysMetadataObject3, SysMetadataHistoryObject as SysMetadataHistoryObject3 } from "@objectstack/metadata-core";
5577
+ import { SysMetadataObject as SysMetadataObject4, SysMetadataHistoryObject as SysMetadataHistoryObject4 } from "@objectstack/metadata-core";
5124
5578
 
5125
5579
  // src/utils/history-cleanup.ts
5126
5580
  import { DEFAULT_METADATA_TYPE_REGISTRY as DEFAULT_METADATA_TYPE_REGISTRY2 } from "@objectstack/spec/kernel";
@@ -5403,11 +5857,12 @@ export {
5403
5857
  migration_exports as Migration,
5404
5858
  NodeMetadataManager,
5405
5859
  RemoteLoader,
5406
- SysMetadataHistoryObject3 as SysMetadataHistoryObject,
5407
- SysMetadataObject3 as SysMetadataObject,
5860
+ SysMetadataHistoryObject4 as SysMetadataHistoryObject,
5861
+ SysMetadataObject4 as SysMetadataObject,
5408
5862
  TypeScriptSerializer,
5409
5863
  YAMLSerializer,
5410
5864
  calculateChecksum,
5865
+ deriveViewContainerObject,
5411
5866
  generateDiffSummary,
5412
5867
  generateSimpleDiff
5413
5868
  };