@objectstack/metadata 17.2.0 → 17.4.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.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) => {
@@ -192,6 +197,9 @@ var init_hmr_routes = __esm({
192
197
  // src/node.ts
193
198
  var node_exports = {};
194
199
  __export(node_exports, {
200
+ AMBIGUOUS_METADATA_STEM_CODE: () => AMBIGUOUS_METADATA_STEM_CODE,
201
+ AMBIGUOUS_METADATA_STEM_STATUS: () => AMBIGUOUS_METADATA_STEM_STATUS,
202
+ AmbiguousMetadataStemError: () => AmbiguousMetadataStemError,
195
203
  DatabaseLoader: () => DatabaseLoader,
196
204
  FilesystemLoader: () => FilesystemLoader,
197
205
  HistoryCleanupManager: () => HistoryCleanupManager,
@@ -202,13 +210,15 @@ __export(node_exports, {
202
210
  Migration: () => migration_exports,
203
211
  NodeMetadataManager: () => NodeMetadataManager,
204
212
  RemoteLoader: () => RemoteLoader,
205
- SysMetadataHistoryObject: () => import_metadata_core3.SysMetadataHistoryObject,
206
- SysMetadataObject: () => import_metadata_core3.SysMetadataObject,
213
+ SysMetadataHistoryObject: () => import_metadata_core4.SysMetadataHistoryObject,
214
+ SysMetadataObject: () => import_metadata_core4.SysMetadataObject,
207
215
  TypeScriptSerializer: () => TypeScriptSerializer,
208
216
  YAMLSerializer: () => YAMLSerializer,
209
217
  calculateChecksum: () => calculateChecksum,
218
+ deriveViewContainerObject: () => deriveViewContainerObject,
210
219
  generateDiffSummary: () => generateDiffSummary,
211
- generateSimpleDiff: () => generateSimpleDiff
220
+ generateSimpleDiff: () => generateSimpleDiff,
221
+ isAmbiguousMetadataStemError: () => isAmbiguousMetadataStemError
212
222
  });
213
223
  module.exports = __toCommonJS(node_exports);
214
224
 
@@ -386,7 +396,7 @@ export default metadata;
386
396
  };
387
397
 
388
398
  // src/loaders/database-loader.ts
389
- var import_metadata_core = require("@objectstack/metadata-core");
399
+ var import_metadata_core2 = require("@objectstack/metadata-core");
390
400
  var import_spec = require("@objectstack/spec");
391
401
  var import_shared = require("@objectstack/spec/shared");
392
402
 
@@ -554,141 +564,49 @@ var LRUCache = class {
554
564
  }
555
565
  };
556
566
 
557
- // src/utils/schema-sync-errors.ts
567
+ // src/loaders/database-loader.ts
558
568
  var import_types = require("@objectstack/types");
559
- var ALREADY_EXISTS = {
560
- codes: /* @__PURE__ */ new Set([
561
- // PostgreSQL SQLSTATE (class 42 — syntax error or access rule violation)
562
- "42P07",
563
- // duplicate_table
564
- "42701",
565
- // duplicate_column
566
- "42710",
567
- // duplicate_object — index / constraint already exists
568
- // MySQL / MariaDB (mysql2 puts the symbolic name on `code`)
569
- "ER_TABLE_EXISTS_ERROR",
570
- // 1050
571
- "ER_DUP_FIELDNAME",
572
- // 1060
573
- "ER_DUP_KEYNAME"
574
- // 1061
575
- ]),
576
- errnos: /* @__PURE__ */ new Set([1050, 1060, 1061]),
577
- /**
578
- * Message fallback for drivers that carry no machine-readable code —
579
- * notably SQLite, whose `code` is the undifferentiated `SQLITE_ERROR` for
580
- * every DDL failure, so the message is the only signal available:
581
- * - `table sys_metadata already exists`
582
- * - `duplicate column name: environment_id`
583
- * - `index idx_x already exists`
584
- * Postgres phrases its own as `relation "x" already exists` /
585
- * `column "x" of relation "y" already exists`, which matches the same test.
586
- */
587
- message: /already exists|duplicate column name|duplicate key name/i
588
- };
589
- var MISSING_TABLE = {
590
- codes: /* @__PURE__ */ new Set([
591
- "42P01",
592
- // PostgreSQL undefined_table
593
- "ER_NO_SUCH_TABLE"
594
- // MySQL / MariaDB 1146
595
- ]),
596
- errnos: /* @__PURE__ */ new Set([1146]),
597
- /**
598
- * - SQLite / libsql: `no such table: sys_metadata_history`
599
- * - PostgreSQL: `relation "sys_metadata_history" does not exist`
600
- * - MySQL/MariaDB: `Table 'app.sys_metadata_history' doesn't exist`
601
- */
602
- message: /no such table|relation ["'`][^"'`]+["'`] does not exist|table ["'`][^"'`]+["'`] doesn'?t exist|unknown table/i,
603
- excludes: {
604
- /**
605
- * Exactly the three SQLSTATEs the docblock above already names as
606
- * must-stay-loud neighbours of `does not exist`. They are listed here
607
- * rather than merely trusted to miss the message test, because two of
608
- * them (42703 columns, 42704 constraints/triggers) have a phrasing that
609
- * *does* hit it, and because a code is a fact where prose is a guess.
610
- *
611
- * Postgres-shaped on purpose: measured, neither MySQL
612
- * (`Unknown column 'label' in 'field list'`) nor SQLite
613
- * (`no such column: bogus`, `table t has no column named label`)
614
- * phrases a sub-object failure so that a missing-table phrase falls out
615
- * of it, so there is nothing there to exclude. Adding their codes would
616
- * be surface with no defect behind it.
617
- */
618
- codes: /* @__PURE__ */ new Set([
619
- "42703",
620
- // undefined_column
621
- "42704",
622
- // undefined_object — constraint, trigger, role, type, …
623
- "3D000"
624
- // invalid_catalog_name — `database "x" does not exist`
625
- ]),
626
- /**
627
- * `«sub-object» "x" of relation "y" …` — Postgres' phrasing for a
628
- * failure about something *inside* a relation, which therefore says the
629
- * relation itself is present. The two in-repo siblings that carry this
630
- * phrase are `mapDataError` (`packages/rest`, #5352) and
631
- * `service-analytics`'s missing-column subtraction (#6035/PR #6346).
632
- *
633
- * [#6615] All three now read one home — `@objectstack/types` — instead
634
- * of three hand-kept copies, so the phrase can no longer be taught to
635
- * the repo a fourth time or drift in one package only. The **width**
636
- * difference that used to justify the copy is preserved and is the
637
- * reason the home exports two functions rather than one: those two
638
- * *extract* the column name to phrase a better error, so a miss costs a
639
- * vaguer message; this one *excludes*, so a miss restores the
640
- * corruption. {@link isRelationSubObjectPhrase} is therefore the wider
641
- * question — it drops their `column`/`[a-z0-9_]+`/`does not exist`
642
- * anchors: any sub-object, any quoted identifier, any verdict.
643
- * Over-matching here only ever converts a benign verdict into a loud
644
- * one, which is the direction this whole module already errs in.
645
- */
646
- matchesMessage: import_types.isRelationSubObjectPhrase
569
+
570
+ // src/migrations/driver-exec.ts
571
+ function resolveDriverExec(driver) {
572
+ const candidate = driver;
573
+ if (!candidate) return void 0;
574
+ if (typeof candidate.execute === "function") {
575
+ return (sql, bindings) => candidate.execute(sql, bindings ? [...bindings] : []);
647
576
  }
648
- };
649
- var MAX_CAUSE_DEPTH = 4;
650
- function matchesDriverError(error, signature, depth) {
651
- if (error === null || error === void 0 || depth > MAX_CAUSE_DEPTH) return false;
652
- if (typeof error === "string") {
653
- if (signature.excludes?.matchesMessage(error)) return false;
654
- return signature.message.test(error);
655
- }
656
- if (typeof error !== "object") return false;
657
- const err = error;
658
- const excludes = signature.excludes;
659
- if (excludes) {
660
- if (typeof err.code === "string" && excludes.codes.has(err.code)) return false;
661
- if (typeof err.message === "string" && excludes.matchesMessage(err.message)) return false;
662
- }
663
- if (typeof err.code === "string" && signature.codes.has(err.code)) return true;
664
- if (typeof err.errno === "number" && signature.errnos.has(err.errno)) return true;
665
- if (typeof err.message === "string" && signature.message.test(err.message)) return true;
666
- return matchesDriverError(err.cause, signature, depth + 1);
667
- }
668
- function isSchemaAlreadyExistsError(error, depth = 0) {
669
- return matchesDriverError(error, ALREADY_EXISTS, depth);
577
+ if (typeof candidate.raw === "function") {
578
+ return (sql, bindings) => candidate.raw(sql, bindings ? [...bindings] : []);
579
+ }
580
+ return void 0;
670
581
  }
671
- function isMissingTableError(error, depth = 0) {
672
- return matchesDriverError(error, MISSING_TABLE, depth);
582
+ function driverExecRefusal(helper) {
583
+ 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.`;
673
584
  }
674
585
 
675
586
  // src/migrations/migrate-project-id-to-environment-id.ts
676
- var AFFECTED_TABLES = [
677
- "sys_metadata",
678
- "sys_metadata_history"
679
- ];
587
+ var import_metadata_core = require("@objectstack/metadata-core");
588
+ var SOURCE_COLUMN = "project_id";
589
+ var TARGET_COLUMN = "environment_id";
590
+ var CANDIDATE_OBJECTS = [import_metadata_core.SysMetadataObject, import_metadata_core.SysMetadataHistoryObject];
591
+ function declaresColumn(object, column) {
592
+ return Object.prototype.hasOwnProperty.call(object.fields ?? {}, column);
593
+ }
594
+ var CANDIDATE_TABLES = CANDIDATE_OBJECTS.map((o) => o.name);
595
+ var AFFECTED_TABLES = CANDIDATE_OBJECTS.filter((o) => declaresColumn(o, TARGET_COLUMN)).map((o) => o.name);
680
596
  async function migrateProjectIdToEnvironmentId(driver) {
681
- const driverAny = driver;
682
- if (typeof driverAny.raw !== "function") {
683
- throw new Error(
684
- "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."
685
- );
597
+ const exec = resolveDriverExec(driver);
598
+ if (!exec) {
599
+ throw new Error(driverExecRefusal("migrateProjectIdToEnvironmentId"));
686
600
  }
687
601
  const results = [];
688
- for (const table of AFFECTED_TABLES) {
602
+ for (const table of CANDIDATE_TABLES) {
603
+ if (!AFFECTED_TABLES.includes(table)) {
604
+ results.push({ table, status: "skipped_not_declared" });
605
+ continue;
606
+ }
689
607
  try {
690
- const hasColumn = await _columnExists(driverAny, table, "project_id");
691
- const alreadyMigrated = await _columnExists(driverAny, table, "environment_id");
608
+ const hasColumn = await _columnExists(exec, table, SOURCE_COLUMN);
609
+ const alreadyMigrated = await _columnExists(exec, table, TARGET_COLUMN);
692
610
  if (alreadyMigrated && !hasColumn) {
693
611
  results.push({ table, status: "already_done" });
694
612
  continue;
@@ -697,8 +615,8 @@ async function migrateProjectIdToEnvironmentId(driver) {
697
615
  results.push({ table, status: "table_missing" });
698
616
  continue;
699
617
  }
700
- await driverAny.raw(
701
- `ALTER TABLE "${table}" RENAME COLUMN project_id TO environment_id`
618
+ await exec(
619
+ `ALTER TABLE "${table}" RENAME COLUMN ${SOURCE_COLUMN} TO ${TARGET_COLUMN}`
702
620
  );
703
621
  results.push({ table, status: "renamed" });
704
622
  } catch (err) {
@@ -707,14 +625,14 @@ async function migrateProjectIdToEnvironmentId(driver) {
707
625
  }
708
626
  return results;
709
627
  }
710
- async function _columnExists(driver, table, column) {
628
+ async function _columnExists(exec, table, column) {
711
629
  try {
712
- const rows = await driver.raw(`PRAGMA table_info("${table}")`);
630
+ const rows = await exec(`PRAGMA table_info("${table}")`);
713
631
  if (Array.isArray(rows) && rows.length > 0) {
714
632
  const list2 = Array.isArray(rows[0]) ? rows[0] : rows;
715
633
  return list2.some((r) => r?.name === column);
716
634
  }
717
- const result = await driver.raw(
635
+ const result = await exec(
718
636
  `SELECT column_name FROM information_schema.columns WHERE table_name = ? AND column_name = ?`,
719
637
  [table, column]
720
638
  );
@@ -726,6 +644,16 @@ async function _columnExists(driver, table, column) {
726
644
  }
727
645
 
728
646
  // src/loaders/database-loader.ts
647
+ function canonicalIsoInstant(value) {
648
+ if (value === null || value === void 0) return void 0;
649
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? void 0 : value.toISOString();
650
+ if (typeof value === "string") return value;
651
+ return String(value);
652
+ }
653
+ function isoFromValidDate(value) {
654
+ if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString();
655
+ return value;
656
+ }
729
657
  var DatabaseLoader = class {
730
658
  constructor(options) {
731
659
  this.contract = {
@@ -774,7 +702,7 @@ var DatabaseLoader = class {
774
702
  if (cacheEnabled) {
775
703
  const lruOpts = {
776
704
  maxSize: cacheOpts?.maxSize ?? 500,
777
- ttl: cacheOpts?.ttl ?? 6e4
705
+ ttl: cacheOpts?.ttlMs ?? 6e4
778
706
  };
779
707
  this.loadCache = new LRUCache(lruOpts);
780
708
  this.loadManyCache = new LRUCache(lruOpts);
@@ -862,9 +790,11 @@ var DatabaseLoader = class {
862
790
  }
863
791
  return this.driver.create(table, data);
864
792
  }
793
+ // `null` is the driver path's not-found answer (`IDataDriver.update()`,
794
+ // #13878); both callers here resolve the row first and discard the result.
865
795
  async _update(table, id, data) {
866
796
  if (this.engine) {
867
- return this.engine.update(table, { id, ...data });
797
+ return this.engine.update(table, { ...data, id });
868
798
  }
869
799
  return this.driver.update(table, id, data);
870
800
  }
@@ -909,7 +839,7 @@ var DatabaseLoader = class {
909
839
  }
910
840
  return max + 1;
911
841
  } catch (error) {
912
- if (isMissingTableError(error)) return 1;
842
+ if ((0, import_types.isMissingTableError)(error, this.historyTableName)) return 1;
913
843
  throw error;
914
844
  }
915
845
  }
@@ -947,11 +877,11 @@ var DatabaseLoader = class {
947
877
  }
948
878
  try {
949
879
  await this.driver.syncSchema(this.tableName, {
950
- ...import_metadata_core.SysMetadataObject,
880
+ ...import_metadata_core2.SysMetadataObject,
951
881
  name: this.tableName
952
882
  });
953
883
  } catch (error) {
954
- if (!isSchemaAlreadyExistsError(error)) {
884
+ if (!(0, import_types.isSchemaAlreadyExistsError)(error)) {
955
885
  if (!this.schemaFailureReported) {
956
886
  this.schemaFailureReported = true;
957
887
  console.error(
@@ -986,7 +916,7 @@ var DatabaseLoader = class {
986
916
  }
987
917
  try {
988
918
  await this.driver.syncSchema(this.historyTableName, {
989
- ...import_metadata_core.SysMetadataHistoryObject,
919
+ ...import_metadata_core2.SysMetadataHistoryObject,
990
920
  name: this.historyTableName
991
921
  });
992
922
  if (this.historySchemaFailureReported) {
@@ -997,7 +927,7 @@ var DatabaseLoader = class {
997
927
  }
998
928
  this.historySchemaReady = true;
999
929
  } catch (error) {
1000
- if (isSchemaAlreadyExistsError(error)) {
930
+ if ((0, import_types.isSchemaAlreadyExistsError)(error)) {
1001
931
  this.historySchemaReady = true;
1002
932
  return;
1003
933
  }
@@ -1155,9 +1085,9 @@ var DatabaseLoader = class {
1155
1085
  source: row.source,
1156
1086
  tags: row.tags ? typeof row.tags === "string" ? JSON.parse(row.tags) : row.tags : void 0,
1157
1087
  createdBy: row.created_by,
1158
- createdAt: row.created_at,
1088
+ createdAt: isoFromValidDate(row.created_at),
1159
1089
  updatedBy: row.updated_by,
1160
- updatedAt: row.updated_at
1090
+ updatedAt: isoFromValidDate(row.updated_at)
1161
1091
  };
1162
1092
  }
1163
1093
  // ==========================================
@@ -1208,7 +1138,7 @@ var DatabaseLoader = class {
1208
1138
  * with its empty value.
1209
1139
  */
1210
1140
  rethrowUnlessTableUnprovisioned(error) {
1211
- if (isMissingTableError(error)) return;
1141
+ if ((0, import_types.isMissingTableError)(error, this.tableName)) return;
1212
1142
  throw error;
1213
1143
  }
1214
1144
  // ==========================================
@@ -1258,17 +1188,38 @@ var DatabaseLoader = class {
1258
1188
  };
1259
1189
  }
1260
1190
  }
1261
- async loadMany(type, _options) {
1191
+ /**
1192
+ * The one type-wide read both plural readers share: every row of `type`, each
1193
+ * body paired with the `name` COLUMN it was stored under.
1194
+ *
1195
+ * [#14205] `name` is `null` only for a row whose key column does not hold a
1196
+ * string. Such a row is still a body {@link loadMany} must return — dropping
1197
+ * it would change what consumers see today — but it has no usable identity,
1198
+ * so {@link loadManyKeyed} filters it out rather than invent one.
1199
+ *
1200
+ * One query and one cache entry serve both methods: `loadMany()` used to own
1201
+ * them, and splitting them would have made every keyed `list()` read miss the
1202
+ * cache and re-hit the database.
1203
+ */
1204
+ async readTypeRows(type) {
1262
1205
  await this.ensureSchema();
1263
1206
  if (this.loadManyCache) {
1264
1207
  const cached = this.loadManyCache.get(type);
1265
- if (cached !== void 0) return cached;
1208
+ if (cached !== void 0) {
1209
+ return cached;
1210
+ }
1266
1211
  }
1267
1212
  try {
1268
1213
  const rows = await this._find(this.tableName, {
1269
1214
  where: this.baseFilter(type)
1270
1215
  });
1271
- const result = rows.map((row) => this.rowToData(row)).filter((data) => data !== null);
1216
+ const result = [];
1217
+ for (const row of rows) {
1218
+ const data = this.rowToData(row);
1219
+ if (data === null) continue;
1220
+ const name = row.name;
1221
+ result.push({ name: typeof name === "string" && name !== "" ? name : null, data });
1222
+ }
1272
1223
  this.loadManyCache?.set(type, result);
1273
1224
  return result;
1274
1225
  } catch (error) {
@@ -1276,6 +1227,29 @@ var DatabaseLoader = class {
1276
1227
  return [];
1277
1228
  }
1278
1229
  }
1230
+ async loadMany(type, _options) {
1231
+ return (await this.readTypeRows(type)).map((entry) => entry.data);
1232
+ }
1233
+ /**
1234
+ * [#14205] The keyed half of {@link loadMany} — see
1235
+ * {@link MetadataKeyedItem} for why the row key travels beside the body
1236
+ * instead of inside it.
1237
+ *
1238
+ * `DatabaseLoader` is where the defect was measured: an aggregated view
1239
+ * container is written by `register('view', OBJECT, container)` and stored
1240
+ * verbatim, so its `sys_metadata` row carries the identity in the `name`
1241
+ * COLUMN and the body has none. {@link rowToData} returns that body without
1242
+ * folding the column in — deliberately, and unchanged here.
1243
+ */
1244
+ async loadManyKeyed(type, _options) {
1245
+ const entries = await this.readTypeRows(type);
1246
+ const keyed = [];
1247
+ for (const entry of entries) {
1248
+ if (entry.name === null) continue;
1249
+ keyed.push({ name: entry.name, data: entry.data });
1250
+ }
1251
+ return keyed;
1252
+ }
1279
1253
  async exists(type, name) {
1280
1254
  await this.ensureSchema();
1281
1255
  if (this.loadCache) {
@@ -1311,7 +1285,7 @@ var DatabaseLoader = class {
1311
1285
  const metadataStr = typeof row.metadata === "string" ? row.metadata : JSON.stringify(row.metadata);
1312
1286
  const stats = {
1313
1287
  size: metadataStr.length,
1314
- mtime: record.updatedAt ?? record.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1288
+ mtime: canonicalIsoInstant(record.updatedAt ?? record.createdAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
1315
1289
  format: "json",
1316
1290
  etag: record.checksum
1317
1291
  };
@@ -1372,7 +1346,7 @@ var DatabaseLoader = class {
1372
1346
  changeNote: row.change_note,
1373
1347
  organizationId: row.organization_id,
1374
1348
  recordedBy: row.recorded_by,
1375
- recordedAt: row.recorded_at
1349
+ recordedAt: isoFromValidDate(row.recorded_at)
1376
1350
  };
1377
1351
  }
1378
1352
  /**
@@ -1429,7 +1403,7 @@ var DatabaseLoader = class {
1429
1403
  changeNote: row.change_note,
1430
1404
  organizationId: row.organization_id,
1431
1405
  recordedBy: row.recorded_by,
1432
- recordedAt: row.recorded_at
1406
+ recordedAt: isoFromValidDate(row.recorded_at)
1433
1407
  };
1434
1408
  });
1435
1409
  return { records: result, total, hasMore };
@@ -1578,6 +1552,33 @@ function generateId() {
1578
1552
  return `meta_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
1579
1553
  }
1580
1554
 
1555
+ // src/loaders/ambiguous-metadata-stem.ts
1556
+ var AMBIGUOUS_METADATA_STEM_CODE = "AMBIGUOUS_METADATA_STEM";
1557
+ var AMBIGUOUS_METADATA_STEM_STATUS = 500;
1558
+ var AMBIGUOUS_METADATA_STEM_BRAND = /* @__PURE__ */ Symbol.for("objectstack.metadata.ambiguousStem");
1559
+ var _a, _b;
1560
+ var AmbiguousMetadataStemError = class extends (_b = Error, _a = AMBIGUOUS_METADATA_STEM_BRAND, _b) {
1561
+ constructor(type, stem, paths) {
1562
+ const sorted = [...paths].sort();
1563
+ super(
1564
+ `Ambiguous metadata name \`${stem}\` for type \`${type}\`: ${sorted.length} files resolve to the same name \u2014 ${sorted.map((p) => `\`${p}\``).join(", ")}. Only the first would ever be served (extension precedence: .json, .yaml, .yml, .ts, .js), so the others are listed and unreachable. Delete or rename all but one.`
1565
+ );
1566
+ /** Brand — see the module doc on why this is not `instanceof`. */
1567
+ this[_a] = true;
1568
+ /** ADR-0112 wire code. */
1569
+ this.code = AMBIGUOUS_METADATA_STEM_CODE;
1570
+ /** HTTP status a transport should answer. */
1571
+ this.status = AMBIGUOUS_METADATA_STEM_STATUS;
1572
+ this.name = "AmbiguousMetadataStemError";
1573
+ this.type = type;
1574
+ this.stem = stem;
1575
+ this.paths = sorted;
1576
+ }
1577
+ };
1578
+ function isAmbiguousMetadataStemError(err) {
1579
+ return typeof err === "object" && err !== null && err[AMBIGUOUS_METADATA_STEM_BRAND] === true;
1580
+ }
1581
+
1581
1582
  // src/endpoint-matcher.ts
1582
1583
  var import_api = require("@objectstack/spec/api");
1583
1584
 
@@ -1725,6 +1726,36 @@ var EndpointMatcher = class {
1725
1726
  }
1726
1727
  };
1727
1728
 
1729
+ // src/view-container-expansion.ts
1730
+ var import_spec2 = require("@objectstack/spec");
1731
+ var import_shared2 = require("@objectstack/spec/shared");
1732
+
1733
+ // src/view-container.ts
1734
+ function deriveViewContainerObject(container) {
1735
+ if (!container || typeof container !== "object") return void 0;
1736
+ const c = container;
1737
+ const own = typeof c.object === "string" && c.object ? c.object : void 0;
1738
+ const byName = typeof c.name === "string" && c.name ? c.name : void 0;
1739
+ return own ?? c?.list?.data?.object ?? c?.form?.data?.object ?? byName;
1740
+ }
1741
+
1742
+ // src/view-container-expansion.ts
1743
+ function expandRuntimeViewContainer(data) {
1744
+ if (!(0, import_spec2.isAggregatedViewContainer)(data)) return [];
1745
+ const container = data;
1746
+ const viewObject = deriveViewContainerObject(container);
1747
+ if (!viewObject) return [];
1748
+ const out = [];
1749
+ for (const vi of (0, import_spec2.expandViewContainer)(viewObject, container)) {
1750
+ (0, import_shared2.applyProtection)(vi, {
1751
+ packageId: container._packageId,
1752
+ packageVersion: container._packageVersion
1753
+ });
1754
+ out.push(vi);
1755
+ }
1756
+ return out;
1757
+ }
1758
+
1728
1759
  // src/metadata-manager.ts
1729
1760
  var WRITABLE_LOADER_METHODS = ["save", "delete"];
1730
1761
  var WRITABLE_LOADER_METHOD_SIGNATURE = {
@@ -1764,8 +1795,6 @@ var _MetadataManager = class _MetadataManager {
1764
1795
  this.watchCallbacks = /* @__PURE__ */ new Map();
1765
1796
  // In-memory metadata registry: type -> name -> data
1766
1797
  this.registry = /* @__PURE__ */ new Map();
1767
- // Overlay storage: "type:name:scope" -> MetadataOverlay
1768
- this.overlays = /* @__PURE__ */ new Map();
1769
1798
  // Type registry for metadata type info
1770
1799
  this.typeRegistry = [];
1771
1800
  // Dependency tracking: "type:name" -> dependencies
@@ -2340,6 +2369,67 @@ var _MetadataManager = class _MetadataManager {
2340
2369
  * result may be memoized depends on what happened to the read's registration
2341
2370
  * while it ran, which only `list()` can see.
2342
2371
  */
2372
+ /**
2373
+ * Merge one loader's answer for `type` into `items`, under the identity that
2374
+ * loader holds each item by.
2375
+ *
2376
+ * ## [#14205] The identity of a loader-held item is its ROW KEY
2377
+ *
2378
+ * Both plural readers used to key a loader's items by `body.name`, and admit
2379
+ * an item only when the body carried a string one:
2380
+ *
2381
+ * ```ts
2382
+ * if (itemAny && typeof itemAny.name === 'string' && !items.has(itemAny.name))
2383
+ * ```
2384
+ *
2385
+ * A body is not required to name itself. `register(type, name, data)` takes
2386
+ * the key as its ARGUMENT, and `assertMetadataRegisterContract` says in as
2387
+ * many words that "A document with NO `name` of its own is fine — the argument
2388
+ * is the key". An aggregated `defineView` container is exactly that: no own
2389
+ * `name` by design, identity carried in the row's `name` column.
2390
+ *
2391
+ * So the old gate dropped every such item the moment the registry went cold
2392
+ * and only the loader could answer — a persisted view container vanished from
2393
+ * `list('view')` after a restart, and `listDiagnosed()` called the short
2394
+ * answer complete because no loader had thrown. Same gate, same effect, in
2395
+ * `listForIndex()`: a nameless `api` row fell out of the endpoint index, where
2396
+ * a miss reads as "nothing declares this route".
2397
+ *
2398
+ * The repair is to ask the loader for the key instead of guessing it from the
2399
+ * body ({@link MetadataLoader.loadManyKeyed}), and to keep the key BESIDE the
2400
+ * body: nothing is written into a body that deliberately has none, so the
2401
+ * register contract's refusal of a disagreeing `data.name` still means what it
2402
+ * says.
2403
+ *
2404
+ * Nothing consumers see today changes shape. For any item that went through
2405
+ * `register()`, a `data.name` that exists is required to EQUAL the key, so the
2406
+ * keyed merge produces the identical map entry; what is new is only the
2407
+ * entries the old gate refused. The `loadMany()` fallback below is the
2408
+ * pre-#14205 behaviour verbatim, for loaders that cannot produce keys
2409
+ * (`RemoteLoader`'s wire format carries bodies only).
2410
+ *
2411
+ * Read failures are NOT caught here: `readListUncached` warns-and-continues,
2412
+ * `listForIndex` deliberately throws, and that difference is each caller's to
2413
+ * keep.
2414
+ */
2415
+ async admitLoaderItems(loader, type, items) {
2416
+ if (typeof loader.loadManyKeyed === "function") {
2417
+ const keyed = await loader.loadManyKeyed(type);
2418
+ for (const entry of keyed) {
2419
+ if (!entry || typeof entry.name !== "string" || entry.name === "") continue;
2420
+ if (items.has(entry.name)) continue;
2421
+ items.set(entry.name, entry.data);
2422
+ }
2423
+ return;
2424
+ }
2425
+ const loaderItems = await loader.loadMany(type);
2426
+ for (const item of loaderItems) {
2427
+ const itemAny = item;
2428
+ if (itemAny && typeof itemAny.name === "string" && !items.has(itemAny.name)) {
2429
+ items.set(itemAny.name, item);
2430
+ }
2431
+ }
2432
+ }
2343
2433
  async readListUncached(type) {
2344
2434
  const items = /* @__PURE__ */ new Map();
2345
2435
  const typeStore = this.registry.get(type);
@@ -2352,15 +2442,12 @@ var _MetadataManager = class _MetadataManager {
2352
2442
  const errors = [];
2353
2443
  for (const loader of this.loaders.values()) {
2354
2444
  try {
2355
- const loaderItems = await loader.loadMany(type);
2356
- for (const item of loaderItems) {
2357
- const itemAny = item;
2358
- if (itemAny && typeof itemAny.name === "string" && !items.has(itemAny.name)) {
2359
- items.set(itemAny.name, item);
2360
- }
2361
- }
2445
+ await this.admitLoaderItems(loader, type, items);
2362
2446
  this.reportLoaderReadRecovered(loader.contract.name);
2363
2447
  } catch (e) {
2448
+ if (isAmbiguousMetadataStemError(e)) {
2449
+ throw e;
2450
+ }
2364
2451
  degraded = true;
2365
2452
  errors.push(`${loader.contract.name}: ${e instanceof Error ? e.message : String(e)}`);
2366
2453
  this.reportLoaderReadFailure(loader.contract.name, type, e);
@@ -2511,13 +2598,7 @@ var _MetadataManager = class _MetadataManager {
2511
2598
  }
2512
2599
  }
2513
2600
  for (const loader of this.loaders.values()) {
2514
- const loaderItems = await loader.loadMany(type);
2515
- for (const item of loaderItems) {
2516
- const itemAny = item;
2517
- if (itemAny && typeof itemAny.name === "string" && !items.has(itemAny.name)) {
2518
- items.set(itemAny.name, item);
2519
- }
2520
- }
2601
+ await this.admitLoaderItems(loader, type, items);
2521
2602
  }
2522
2603
  return Array.from(items.values());
2523
2604
  }
@@ -2704,6 +2785,30 @@ var _MetadataManager = class _MetadataManager {
2704
2785
  }
2705
2786
  /**
2706
2787
  * List all names of metadata items of a given type
2788
+ *
2789
+ * ## [#14423] One loader's fault does not take the whole enumeration down
2790
+ *
2791
+ * This loop used to be bare — `const result = await loader.list(type)` with
2792
+ * no `try`, while the two sibling plural reads (`list()` via
2793
+ * {@link admitLoaderItems}, and {@link loadMany}) have carried a per-loader
2794
+ * `catch` since #5108. That asymmetry is the defect, independent of any one
2795
+ * caller: the SAME storage outage was swallowed by one plural read and
2796
+ * thrown out of the other, so which answer a caller got depended only on
2797
+ * which method it happened to call. A caller reading both — the action
2798
+ * governance audit is one — saw `loadMany` report a short-but-successful
2799
+ * set and `listNames` throw, and had no way to tell that one fact was
2800
+ * behind both.
2801
+ *
2802
+ * Same shape as `loadMany`'s, deliberately, down to the helpers: the outage
2803
+ * is spoken once per loader through {@link reportLoaderReadFailure} and
2804
+ * un-said through {@link reportLoaderReadRecovered}. ⛔ Not a third spelling
2805
+ * for "a loader faulted" — a second vocabulary for one event is how the two
2806
+ * reads drifted apart in the first place.
2807
+ *
2808
+ * The degradation is the same one `list()` documents and is graded the same
2809
+ * way (AGENTS.md → "Degradation log levels"): the caller still gets an
2810
+ * array, nothing 500s, and the set is quietly short — so it is reported at
2811
+ * `error`, by the shared helper, rather than being re-graded here.
2707
2812
  */
2708
2813
  async listNames(type) {
2709
2814
  type = (0, import_core.canonicalMetadataServiceType)(type);
@@ -2715,8 +2820,16 @@ var _MetadataManager = class _MetadataManager {
2715
2820
  }
2716
2821
  }
2717
2822
  for (const loader of this.loaders.values()) {
2718
- const result = await loader.list(type);
2719
- result.forEach((item) => names.add(item));
2823
+ try {
2824
+ const result = await loader.list(type);
2825
+ result.forEach((item) => names.add(item));
2826
+ this.reportLoaderReadRecovered(loader.contract.name);
2827
+ } catch (e) {
2828
+ if (isAmbiguousMetadataStemError(e)) {
2829
+ throw e;
2830
+ }
2831
+ this.reportLoaderReadFailure(loader.contract.name, type, e);
2832
+ }
2720
2833
  }
2721
2834
  return Array.from(names);
2722
2835
  }
@@ -2762,12 +2875,54 @@ var _MetadataManager = class _MetadataManager {
2762
2875
  * Runtime-authored `shared` / `personal` views (`sys_view_definition`) are
2763
2876
  * merged in by the REST layer; this method returns the `package` layer that
2764
2877
  * was registered from source.
2878
+ *
2879
+ * ## [#13913] Aggregated containers are expanded inline, per read
2880
+ *
2881
+ * `this.list('view')` is `MetadataManager`'s OWN loader-based store — the
2882
+ * in-memory registry plus every registered loader — and is a completely
2883
+ * different store from the `sys_metadata` rows `getMetaItems` reads. #13407
2884
+ * taught `getMetaItems` to expand a runtime-authored aggregated container
2885
+ * inline; this exit never called it and had no equivalent step, so a
2886
+ * container that `GET /meta/view?object=` now serves still answered **empty**
2887
+ * here.
2888
+ *
2889
+ * Merely getting the container into the store would not have helped: the
2890
+ * filter also requires `viewKind`, and a container has none. Loosening that
2891
+ * requirement is NOT the repair — it would answer with the container itself
2892
+ * as a view, the behaviour #7163 ruled wrong — so what is added below is the
2893
+ * container's **expansion**, whose items each carry the `viewKind` + `object`
2894
+ * pair this filter has always tested. The filter itself is untouched: it
2895
+ * reads the top-level `object`, exactly as `ViewSchema.object` declares.
2896
+ *
2897
+ * Registry-free and per-read, mirroring #13407's choice at the other exit and
2898
+ * for the same reason — the registry is process-wide, so a read must not
2899
+ * graft rows into it (see `view-container-expansion.ts`'s header, which also
2900
+ * records why the protocol's copy of this logic cannot be imported).
2901
+ *
2902
+ * Already-present items win: an expansion contributes only names the store
2903
+ * does not already hold, so a container whose expanded ViewItems were
2904
+ * registered by a source registrar (the ObjectQL boot loop, the artifact/HMR
2905
+ * loader) still answers with those registered, fully-enriched items and this
2906
+ * step adds nothing.
2765
2907
  */
2766
2908
  async getViewsByObject(object) {
2767
2909
  const views = await this.list("view");
2768
- return views.filter(
2910
+ const matches = views.filter(
2769
2911
  (v) => v && typeof v === "object" && v.viewKind && v.object === object
2770
- ).sort(
2912
+ );
2913
+ const known = /* @__PURE__ */ new Set();
2914
+ for (const v of views) {
2915
+ if (v && typeof v === "object" && typeof v.name === "string") known.add(v.name);
2916
+ }
2917
+ for (const v of views) {
2918
+ for (const item of expandRuntimeViewContainer(v)) {
2919
+ if (!item.viewKind || item.object !== object) continue;
2920
+ if (known.has(item.name)) continue;
2921
+ known.add(item.name);
2922
+ matches.push(item);
2923
+ }
2924
+ }
2925
+ return matches.sort(
2771
2926
  (a, b) => (a.order ?? 0) - (b.order ?? 0) || String(a.name).localeCompare(String(b.name))
2772
2927
  );
2773
2928
  }
@@ -3185,66 +3340,18 @@ var _MetadataManager = class _MetadataManager {
3185
3340
  };
3186
3341
  }
3187
3342
  // ==========================================
3188
- // Overlay / Customization Management
3343
+ // Overlay / Customization Management — REMOVED (#13135, ADR-0049)
3189
3344
  // ==========================================
3190
- overlayKey(type, name, scope = "platform") {
3191
- return `${encodeURIComponent(type)}:${encodeURIComponent(name)}:${scope}`;
3192
- }
3193
- /**
3194
- * Get the active overlay for a metadata item
3195
- */
3196
- async getOverlay(type, name, scope) {
3197
- return this.overlays.get(this.overlayKey(type, name, scope ?? "platform"));
3198
- }
3199
- /**
3200
- * Save/update an overlay for a metadata item
3201
- */
3202
- async saveOverlay(overlay) {
3203
- if (this.config.persistence?.overlayWritable === false) {
3204
- const msg = `MetadataManager overlays are read-only (persistence.overlayWritable=false); refusing to save overlay for ${overlay.baseType}/${overlay.baseName}`;
3205
- if (this.config.validation?.throwOnError) {
3206
- throw new Error(msg);
3207
- }
3208
- this.logger.warn(msg);
3209
- return;
3210
- }
3211
- const key = this.overlayKey(overlay.baseType, overlay.baseName, overlay.scope);
3212
- this.overlays.set(key, overlay);
3213
- }
3214
- /**
3215
- * Remove an overlay, reverting to the base definition
3216
- */
3217
- async removeOverlay(type, name, scope) {
3218
- this.overlays.delete(this.overlayKey(type, name, scope ?? "platform"));
3219
- }
3220
- /**
3221
- * Get the effective (merged) metadata after applying all overlays.
3222
- * Resolution order: system ← merge(platform) ← merge(user)
3223
- */
3224
- async getEffective(type, name, context) {
3225
- const base = await this.get(type, name);
3226
- if (!base) return void 0;
3227
- let effective = { ...base };
3228
- const platformOverlay = await this.getOverlay(type, name, "platform");
3229
- if (platformOverlay?.active && platformOverlay.patch) {
3230
- effective = { ...effective, ...platformOverlay.patch };
3231
- }
3232
- if (context?.userId) {
3233
- const userOverlayKey = this.overlayKey(type, name, "user") + `:${context.userId}`;
3234
- const userOverlay = this.overlays.get(userOverlayKey) ?? await this.getOverlay(type, name, "user");
3235
- if (userOverlay?.active && userOverlay.patch) {
3236
- if (!userOverlay.owner || userOverlay.owner === context.userId) {
3237
- effective = { ...effective, ...userOverlay.patch };
3238
- }
3239
- }
3240
- } else {
3241
- const userOverlay = await this.getOverlay(type, name, "user");
3242
- if (userOverlay?.active && userOverlay.patch && !userOverlay.owner) {
3243
- effective = { ...effective, ...userOverlay.patch };
3244
- }
3245
- }
3246
- return effective;
3247
- }
3345
+ //
3346
+ // The in-memory overlay limb (`getOverlay` / `saveOverlay` / `removeOverlay`
3347
+ // / `getEffective`, keyed `type:name:scope`) implemented the paper
3348
+ // metadata-customization protocol removed from `@objectstack/spec` in the
3349
+ // same change: no route ever served the paper `.../overlay` or
3350
+ // `.../effective` endpoints, and the only callers of these methods were this
3351
+ // package's own unit tests. ADR-0126 supersedes the protocol on the record.
3352
+ // The org-scoped customization that actually ships is ADR-0005's
3353
+ // `sys_metadata` overlay (`getMetaItemLayered` in metadata-protocol), which
3354
+ // never lived here.
3248
3355
  // ==========================================
3249
3356
  // Watch / Subscribe (IMetadataService)
3250
3357
  // ==========================================
@@ -3582,6 +3689,99 @@ var _MetadataManager = class _MetadataManager {
3582
3689
  }
3583
3690
  return results;
3584
3691
  }
3692
+ /**
3693
+ * [#14423] {@link loadMany}, read under the identity the STORE holds each
3694
+ * item by — the keyed plural read, beside the unkeyed one.
3695
+ *
3696
+ * ## Why a second method and not a widened `loadMany`
3697
+ *
3698
+ * `loadMany` keys nothing: it returns bodies, and every consumer that needs
3699
+ * an identity reads `body.name` off them. #14205 already ruled what identity
3700
+ * IS — the key the store holds the item under (`register(type, name, data)`
3701
+ * takes it as the ARGUMENT, and a body is not required to name itself) — so
3702
+ * `body.name` is a guess that happens to be right for most items and drops
3703
+ * the rest ENTIRELY: an item whose body carries no `name` is served by
3704
+ * `load(type, name)` and is not nameable from `loadMany`'s answer at all.
3705
+ *
3706
+ * Widening `loadMany`'s return would fix that and break every consumer of a
3707
+ * published shape (the ones counted on this card all read `body.name` as the
3708
+ * identity). So this is additive: `loadMany`'s return shape is untouched,
3709
+ * and a caller that needs the key asks for the key.
3710
+ *
3711
+ * ## What it reads — the same population `loadMany` reads
3712
+ *
3713
+ * Loaders only, deliberately, so this is `loadMany` keyed and nothing more.
3714
+ * It is NOT `list()`/{@link listNames}, which also merge the in-memory
3715
+ * `register()` registry; a caller wanting that set has those. Reading the
3716
+ * loaders alone is also what makes this the enumerable twin of
3717
+ * {@link loadDiagnosed}, which walks the same loaders by name — that pairing
3718
+ * is the point on the audit side of #14423, where an enumeration and a
3719
+ * by-name read that disagree about a population make one subsystem accuse
3720
+ * another of a defect neither has.
3721
+ *
3722
+ * ## Delegate first, fall back second — and why that order is not a style
3723
+ *
3724
+ * Per loader: {@link MetadataLoader.loadManyKeyed} where the loader offers
3725
+ * one, else its `list()` + a per-name `load()`. Measured, on
3726
+ * `DatabaseLoader`: the keyed method shares `loadMany`'s single query
3727
+ * (`{find:1, findOne:0}` — zero extra cost), while enumerate-then-read-each
3728
+ * on that same loader is a real N+1 (`{find:1, findOne:5}` for five items).
3729
+ * The fallback exists for loaders that cannot produce keys at all
3730
+ * (`RemoteLoader`'s wire format carries bodies only), and it recovers the
3731
+ * nameless item the pre-#14205 `loadMany`-and-key-by-`body.name` fallback
3732
+ * drops — which is why it is `list()` + `load()` and not `loadMany()`.
3733
+ *
3734
+ * ## Failure posture
3735
+ *
3736
+ * Per-loader `try`/`catch`, the same seam and the same helpers as
3737
+ * {@link loadMany} and `list()` — one loader's outage does not take the
3738
+ * enumeration down, and it is reported once through
3739
+ * {@link reportLoaderReadFailure} rather than in a third vocabulary.
3740
+ * Earlier loaders win a key collision, mirroring `list()`.
3741
+ */
3742
+ async loadManyKeyed(type, options) {
3743
+ const items = /* @__PURE__ */ new Map();
3744
+ for (const loader of this.loaders.values()) {
3745
+ try {
3746
+ await this.admitKeyedLoaderItems(loader, type, items, options);
3747
+ this.reportLoaderReadRecovered(loader.contract.name);
3748
+ } catch (e) {
3749
+ this.reportLoaderReadFailure(loader.contract.name, type, e);
3750
+ }
3751
+ }
3752
+ return Array.from(items, ([name, data]) => ({ name, data }));
3753
+ }
3754
+ /**
3755
+ * Merge ONE loader's answer for `type` into `items`, keyed by that loader's
3756
+ * own key for each item — {@link loadManyKeyed}'s per-loader body.
3757
+ *
3758
+ * Distinct from {@link admitLoaderItems} on exactly one axis, and that axis
3759
+ * is the whole of #14423: the fallback for a loader with no
3760
+ * `loadManyKeyed`. `admitLoaderItems` falls back to `loadMany` keyed by
3761
+ * `data.name` — the pre-#14205 behaviour, verbatim, which drops a nameless
3762
+ * body. Here the fallback is `list()` + a per-name `load()`, so a loader
3763
+ * that cannot enumerate keys and bodies together still answers with both.
3764
+ *
3765
+ * Read failures are NOT caught here — the caller owns that verdict, as in
3766
+ * {@link admitLoaderItems}.
3767
+ */
3768
+ async admitKeyedLoaderItems(loader, type, items, options) {
3769
+ if (typeof loader.loadManyKeyed === "function") {
3770
+ const keyed = await loader.loadManyKeyed(type, options);
3771
+ for (const entry of keyed) {
3772
+ if (!entry || typeof entry.name !== "string" || entry.name === "") continue;
3773
+ if (items.has(entry.name)) continue;
3774
+ items.set(entry.name, entry.data);
3775
+ }
3776
+ return;
3777
+ }
3778
+ for (const name of await loader.list(type)) {
3779
+ if (typeof name !== "string" || name === "" || items.has(name)) continue;
3780
+ const result = await loader.load(type, name, options);
3781
+ if (result?.data == null) continue;
3782
+ items.set(name, result.data);
3783
+ }
3784
+ }
3585
3785
  /**
3586
3786
  * Save metadata item to a loader
3587
3787
  */
@@ -4018,6 +4218,7 @@ var MetadataManager = _MetadataManager;
4018
4218
  // src/plugin.ts
4019
4219
  var import_promises = require("fs/promises");
4020
4220
  var import_node_crypto2 = require("crypto");
4221
+ var import_core2 = require("@objectstack/core");
4021
4222
 
4022
4223
  // src/node-metadata-manager.ts
4023
4224
  var path2 = __toESM(require("path"), 1);
@@ -4028,7 +4229,11 @@ var fs = __toESM(require("fs/promises"), 1);
4028
4229
  var path = __toESM(require("path"), 1);
4029
4230
  var import_glob = require("glob");
4030
4231
  var import_node_crypto = require("crypto");
4031
- var FilesystemLoader = class {
4232
+ function ownNameOf(data) {
4233
+ const own = data?.name;
4234
+ return typeof own === "string" && own !== "" ? own : null;
4235
+ }
4236
+ var _FilesystemLoader = class _FilesystemLoader {
4032
4237
  constructor(rootDir, serializers, logger) {
4033
4238
  this.rootDir = rootDir;
4034
4239
  this.serializers = serializers;
@@ -4126,6 +4331,82 @@ var FilesystemLoader = class {
4126
4331
  }
4127
4332
  }
4128
4333
  async loadMany(type, options) {
4334
+ return (await this.loadManyEntries(type, options)).map((entry) => entry.data);
4335
+ }
4336
+ /**
4337
+ * [#14341] The keyed half of {@link loadMany} — see {@link MetadataKeyedItem}
4338
+ * for why the store's key travels BESIDE the body instead of being folded
4339
+ * into it.
4340
+ *
4341
+ * THE RULE, in one sentence: an item is keyed by this loader's own
4342
+ * name-to-path derivation — {@link nameFromFilename}, the very basename
4343
+ * derivation `list()` reports — ONLY where that derivation is a bijection for
4344
+ * the file (it sits directly under `ROOT/TYPE/` and carries one of the
4345
+ * extensions {@link findFile} tries, so `findFile(type, key)` resolves back to
4346
+ * this same file); every other shape keeps the pre-#14205 behaviour verbatim,
4347
+ * keyed by `body.name` when it has one and dropped when it has none.
4348
+ *
4349
+ * Why the rule stops there (PM ruling on #14341, 2026-09-02, knowingly over
4350
+ * triage's "a nested path keeps whatever `list()` reports for it today"):
4351
+ * `list()` and `findFile()` DISAGREE outside that shape. For
4352
+ * `ROOT/TYPE/crm/account.json`, `list()` reports the bare `account`, but
4353
+ * `findFile()` resolves that name against `ROOT/TYPE/account.json` and finds
4354
+ * nothing — the only name reaching the file is `crm/account`, which nothing
4355
+ * reports. An extension-less file is read by `loadMany()` and reported by
4356
+ * `list()`, and `findFile()` resolves neither. Keying by either side would
4357
+ * mint a name some other door cannot open, and two directories holding the
4358
+ * same basename would collide in silence
4359
+ * (`MetadataManager.admitLoaderItems()` keeps the first and says nothing).
4360
+ * The card's own fence: "keying items under names nothing else uses … is
4361
+ * worse than today's honest drop". So the drop stays exactly where the key is
4362
+ * unsettled, and is pinned as a RECORD in
4363
+ * `filesystem-loader-keyed-items.test.ts`.
4364
+ *
4365
+ * [#14486, partial] `list()` and {@link findFile} have since converged on
4366
+ * {@link resolvableNameForPath} — the derivation this method already used —
4367
+ * so a nested or extension-less file is now neither listed nor resolvable.
4368
+ * What did NOT change is the WALK behind this method: `loadManyEntries()`
4369
+ * still READS those files, so `loadMany()` still returns their bodies and
4370
+ * this method still falls back to `body.name` for them. That half of the
4371
+ * #14486 ruling ("nothing unlisted is returned by `loadMany()` either") is
4372
+ * deliberately NOT taken here: it would invert the three landed #14341 pins
4373
+ * in `filesystem-loader-keyed-items.test.ts:113,167,187` and the
4374
+ * `loadMany()` CONTROL at `:196`, and that file was under a concurrent
4375
+ * claim (PR #14627) when this landed. The remaining divergence — listed ⊂
4376
+ * loaded — is pinned as a RECORD in
4377
+ * `filesystem-loader-list-reachability.test.ts` rather than left implicit.
4378
+ *
4379
+ * One consequence, deliberate: a flat file whose `body.name` DISAGREES with
4380
+ * its basename is now keyed by the BASENAME. That is #14205's rule (identity
4381
+ * is the key the store holds an item under, not `body.name`) applied to this
4382
+ * loader, and it aligns `MetadataManager.list()` with `listNames()` for that
4383
+ * shape.
4384
+ *
4385
+ * The body is handed back by reference, unchanged: nothing is written into a
4386
+ * body that deliberately has no `name`. `limit` bounds the items LOADED,
4387
+ * exactly as `loadMany()` does — an entry the key rule drops has still been
4388
+ * read and still counts against it.
4389
+ */
4390
+ async loadManyKeyed(type, options) {
4391
+ const typeDir = path.join(this.rootDir, type);
4392
+ const keyed = [];
4393
+ for (const entry of await this.loadManyEntries(type, options)) {
4394
+ const name = this.resolvableNameForPath(typeDir, entry.file) ?? ownNameOf(entry.data);
4395
+ if (name) {
4396
+ keyed.push({ name, data: entry.data });
4397
+ }
4398
+ }
4399
+ return keyed;
4400
+ }
4401
+ /**
4402
+ * The single walk behind {@link loadMany} and {@link loadManyKeyed}: one glob,
4403
+ * one serializer pass, one `limit`. Shared so the two can never answer with
4404
+ * different bodies for the same file — {@link MetadataLoader.loadManyKeyed}
4405
+ * requires `data` to be "the same body `loadMany()` would return for the
4406
+ * item", and a second copy of this walk is how that would quietly stop being
4407
+ * true.
4408
+ */
4409
+ async loadManyEntries(type, options) {
4129
4410
  const { patterns = ["**/*"], recursive: _recursive = true, limit } = options || {};
4130
4411
  const typeDir = path.join(this.rootDir, type);
4131
4412
  const items = [];
@@ -4133,33 +4414,34 @@ var FilesystemLoader = class {
4133
4414
  const globPatterns = patterns.map(
4134
4415
  (pattern) => path.join(typeDir, pattern)
4135
4416
  );
4417
+ const files = [];
4136
4418
  for (const pattern of globPatterns) {
4137
- const files = await (0, import_glob.glob)(pattern, {
4138
- ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*", "**/*[*]*"],
4139
- nodir: true
4140
- });
4141
- for (const file of files) {
4142
- if (limit && items.length >= limit) {
4143
- break;
4144
- }
4145
- try {
4146
- const content = await fs.readFile(file, "utf-8");
4147
- const format = this.detectFormat(file);
4148
- const serializer = this.getSerializer(format);
4149
- if (serializer) {
4150
- const data = serializer.deserialize(content);
4151
- items.push(data);
4152
- }
4153
- } catch (error) {
4154
- this.logger?.warn("Failed to load file", {
4155
- file,
4156
- error: error instanceof Error ? error.message : String(error)
4157
- });
4158
- }
4159
- }
4419
+ files.push(
4420
+ ...await (0, import_glob.glob)(pattern, {
4421
+ ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*", "**/*[*]*"],
4422
+ nodir: true
4423
+ })
4424
+ );
4425
+ }
4426
+ this.resolvableNames(type, typeDir, files);
4427
+ for (const file of files) {
4160
4428
  if (limit && items.length >= limit) {
4161
4429
  break;
4162
4430
  }
4431
+ try {
4432
+ const content = await fs.readFile(file, "utf-8");
4433
+ const format = this.detectFormat(file);
4434
+ const serializer = this.getSerializer(format);
4435
+ if (serializer) {
4436
+ const data = serializer.deserialize(content);
4437
+ items.push({ file, data });
4438
+ }
4439
+ } catch (error) {
4440
+ this.logger?.warn("Failed to load file", {
4441
+ file,
4442
+ error: error instanceof Error ? error.message : String(error)
4443
+ });
4444
+ }
4163
4445
  }
4164
4446
  return items;
4165
4447
  } catch (error) {
@@ -4202,19 +4484,39 @@ var FilesystemLoader = class {
4202
4484
  return null;
4203
4485
  }
4204
4486
  }
4487
+ /**
4488
+ * [#14486] The names this loader can be asked for, and ONLY those: a file
4489
+ * directly under `ROOT/TYPE/` carrying an extension one of this instance's
4490
+ * REGISTERED serializers claims. Every name it reports resolves back through
4491
+ * {@link findFile}, so `listNames()` and `get()` give the same answer.
4492
+ *
4493
+ * It used to report `path.basename(file, ext)` for every file the glob found,
4494
+ * nested or not, extension or not — and {@link findFile} resolves neither
4495
+ * shape. `ROOT/TYPE/crm/account.json` was listed as `account`, which resolves
4496
+ * against `ROOT/TYPE/account.json` and finds nothing; an extension-less
4497
+ * `ROOT/TYPE/noext` was listed as `noext`, which resolves under no appended
4498
+ * extension at all. A name in the list that `get()` answers `null` for is the
4499
+ * silent failure an author (human or AI) reads as their own typo, so they
4500
+ * retry the same word: the list and the door now agree instead.
4501
+ *
4502
+ * Ruling (maintainer, via the director seat on #14486, 2026-09-02): narrow
4503
+ * the list — direction A, over B (reverse-unify: report `crm/account` and
4504
+ * teach `findFile()` path-shaped names), which would have made a slash inside
4505
+ * a metadata name every consumer's permanent obligation with no measured
4506
+ * demand for it. The two-segment layout follows ADR-0008 §10, which
4507
+ * `metadata-fs`'s `parseItemPath()` already enforces for its own store; the
4508
+ * EXTENSION set deliberately does NOT follow §10's `.json`-only rule — see
4509
+ * {@link resolvableExtensions} for why.
4510
+ */
4205
4511
  async list(type) {
4206
4512
  const typeDir = path.join(this.rootDir, type);
4513
+ let files;
4207
4514
  try {
4208
- const files = await (0, import_glob.glob)("**/*", {
4515
+ files = await (0, import_glob.glob)("**/*", {
4209
4516
  cwd: typeDir,
4210
4517
  ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*"],
4211
4518
  nodir: true
4212
4519
  });
4213
- return files.map((file) => {
4214
- const ext = path.extname(file);
4215
- const basename3 = path.basename(file, ext);
4216
- return basename3;
4217
- });
4218
4520
  } catch (error) {
4219
4521
  this.logger?.error("Failed to list", void 0, {
4220
4522
  type,
@@ -4222,6 +4524,7 @@ var FilesystemLoader = class {
4222
4524
  });
4223
4525
  return [];
4224
4526
  }
4527
+ return this.resolvableNames(type, typeDir, files.map((file) => path.join(typeDir, file)));
4225
4528
  }
4226
4529
  async save(type, name, data, options) {
4227
4530
  const startTime = Date.now();
@@ -4292,12 +4595,109 @@ var FilesystemLoader = class {
4292
4595
  throw error;
4293
4596
  }
4294
4597
  }
4598
+ /**
4599
+ * [#14486] The extensions a name can be resolved under, for THIS instance:
4600
+ * the ones belonging to the serializer set it was constructed with. Shared by
4601
+ * {@link findFile}, {@link resolvableNameForPath} and therefore {@link list},
4602
+ * so the set a name can be RESOLVED under cannot drift from the set that is
4603
+ * LISTED or the set {@link loadManyKeyed} is willing to KEY by.
4604
+ *
4605
+ * Registered, not hard-coded, and deliberately not ADR-0008 §10's `.json`
4606
+ * only. §10 governs the `metadata-fs` store; applying it verbatim here would
4607
+ * drop `.yaml` and `.ts` metadata out of `listNames()` — a breakage this card
4608
+ * never asked for. Under the manager's DEFAULT format set
4609
+ * (`typescript` / `json` / `yaml`, `metadata-manager.ts`) that leaves `.js`
4610
+ * out, which is the card's row-4 membership mismatch closing for free: a `.js`
4611
+ * file was listed and resolvable while `loadMany()` could never return it and
4612
+ * `load()` threw `No serializer found for format: javascript`. Register
4613
+ * `javascript` and it is listed, resolvable and loadable together.
4614
+ */
4615
+ resolvableExtensions() {
4616
+ const extensions = [];
4617
+ for (const [format, formatExtensions] of _FilesystemLoader.EXTENSIONS_BY_FORMAT) {
4618
+ if (this.serializers.has(format)) {
4619
+ extensions.push(...formatExtensions);
4620
+ }
4621
+ }
4622
+ return extensions;
4623
+ }
4624
+ /**
4625
+ * The metadata name this loader reports for a file: the basename with its
4626
+ * extension stripped. One derivation, shared by {@link list} and
4627
+ * {@link loadManyKeyed}, so the two cannot drift for the shape where they
4628
+ * agree — `dotted.config.json` is `dotted.config` for both.
4629
+ */
4630
+ static nameFromFilename(file) {
4631
+ return path.basename(file, path.extname(file));
4632
+ }
4633
+ /**
4634
+ * The key for a file IF this loader's name-to-path mapping is a bijection for
4635
+ * it: a file directly under `ROOT/TYPE/` carrying an extension
4636
+ * {@link findFile} tries, so `findFile(type, key)` resolves back to this very
4637
+ * file. `null` for every other shape — a nested path, an extension-less file,
4638
+ * an extension spelled in a case `findFile()` does not compose — which is why
4639
+ * {@link loadManyKeyed} falls back to `body.name` there rather than minting a
4640
+ * key no other door can open.
4641
+ */
4642
+ resolvableNameForPath(typeDir, file) {
4643
+ const rel = path.relative(typeDir, file);
4644
+ if (rel === "" || rel.split(path.sep).length !== 1) {
4645
+ return null;
4646
+ }
4647
+ if (!this.resolvableExtensions().includes(path.extname(rel))) {
4648
+ return null;
4649
+ }
4650
+ return _FilesystemLoader.nameFromFilename(rel);
4651
+ }
4652
+ /**
4653
+ * [#14921] The names this loader reports for `files` — and the ONE place an
4654
+ * ambiguous stem is refused.
4655
+ *
4656
+ * Shared by {@link list} and {@link loadManyEntries} so the two can never
4657
+ * disagree about which trees are admissible: a stem that `list()` refuses
4658
+ * must not still be walked and returned as two bodies by `loadMany()`, which
4659
+ * is exactly the split this card measured.
4660
+ *
4661
+ * Refuses on the FIRST colliding name in sorted order, so a tree holding more
4662
+ * than one collision always names the same one — a refusal that moves
4663
+ * between runs reads as flakiness rather than as the fixed authoring error it
4664
+ * is. Paths are deduplicated because two overlapping `patterns` legitimately
4665
+ * match one file twice, and counting that as a collision would refuse a
4666
+ * perfectly good tree.
4667
+ *
4668
+ * ⛔ Not a precedence resolver. Picking a winner here is what the ruling
4669
+ * declined (option 2, keep the precedence and log): the loser would stay
4670
+ * unreachable and the listed set would stay different from the addressable
4671
+ * one.
4672
+ */
4673
+ resolvableNames(type, typeDir, files) {
4674
+ const byName = /* @__PURE__ */ new Map();
4675
+ for (const file of files) {
4676
+ const name = this.resolvableNameForPath(typeDir, file);
4677
+ if (name === null) {
4678
+ continue;
4679
+ }
4680
+ let paths = byName.get(name);
4681
+ if (!paths) {
4682
+ paths = /* @__PURE__ */ new Set();
4683
+ byName.set(name, paths);
4684
+ }
4685
+ paths.add(file);
4686
+ }
4687
+ for (const name of [...byName.keys()].sort()) {
4688
+ const paths = byName.get(name);
4689
+ if (paths.size > 1) {
4690
+ throw new AmbiguousMetadataStemError(type, name, [...paths]);
4691
+ }
4692
+ }
4693
+ return [...byName.keys()];
4694
+ }
4295
4695
  /**
4296
4696
  * Find file for a given type and name
4297
4697
  */
4298
4698
  async findFile(type, name) {
4299
4699
  const typeDir = path.join(this.rootDir, type);
4300
- const extensions = [".json", ".yaml", ".yml", ".ts", ".js"];
4700
+ const extensions = this.resolvableExtensions();
4301
4701
  for (const ext of extensions) {
4302
4702
  const filePath = path.join(typeDir, `${name}${ext}`);
4303
4703
  try {
@@ -4343,6 +4743,19 @@ var FilesystemLoader = class {
4343
4743
  return `"${hash}"`;
4344
4744
  }
4345
4745
  };
4746
+ /**
4747
+ * The inverse of {@link detectFormat}: which file extensions carry which
4748
+ * format. Fixed ORDER, because it is also {@link findFile}'s precedence when
4749
+ * two files under one type directory share a stem — registration order must
4750
+ * not be able to change which file `ROOT/TYPE/NAME` opens.
4751
+ */
4752
+ _FilesystemLoader.EXTENSIONS_BY_FORMAT = [
4753
+ ["json", [".json"]],
4754
+ ["yaml", [".yaml", ".yml"]],
4755
+ ["typescript", [".ts"]],
4756
+ ["javascript", [".js"]]
4757
+ ];
4758
+ var FilesystemLoader = _FilesystemLoader;
4346
4759
 
4347
4760
  // src/node-metadata-manager.ts
4348
4761
  var NodeMetadataManager = class extends MetadataManager {
@@ -4467,6 +4880,20 @@ var MemoryLoader = class {
4467
4880
  if (!typeStore) return [];
4468
4881
  return Array.from(typeStore.values());
4469
4882
  }
4883
+ /**
4884
+ * [#14205] The keyed half of {@link loadMany}. The storage map is already
4885
+ * `Type -> Name -> Data`, so the key this loader holds an item under is the
4886
+ * map key — `loadMany()` was simply discarding it, which dropped every
4887
+ * nameless body out of `MetadataManager.list()` and out of the endpoint index.
4888
+ *
4889
+ * The body is handed back by reference, unchanged: the key travels beside it,
4890
+ * never folded into it.
4891
+ */
4892
+ async loadManyKeyed(type, _options) {
4893
+ const typeStore = this.storage.get(type);
4894
+ if (!typeStore) return [];
4895
+ return Array.from(typeStore, ([name, data]) => ({ name, data }));
4896
+ }
4470
4897
  async exists(type, name) {
4471
4898
  return this.storage.get(type)?.has(name) ?? false;
4472
4899
  }
@@ -4513,20 +4940,20 @@ var MemoryLoader = class {
4513
4940
 
4514
4941
  // src/plugin.ts
4515
4942
  var import_kernel2 = require("@objectstack/spec/kernel");
4516
- var import_shared2 = require("@objectstack/spec/shared");
4517
- var import_metadata_core2 = require("@objectstack/metadata-core");
4518
- var import_spec2 = require("@objectstack/spec");
4943
+ var import_shared3 = require("@objectstack/spec/shared");
4944
+ var import_metadata_core3 = require("@objectstack/metadata-core");
4519
4945
  var import_spec3 = require("@objectstack/spec");
4946
+ var import_spec4 = require("@objectstack/spec");
4520
4947
  var queryableMetadataObjects = [
4521
- import_metadata_core2.SysMetadataObject,
4522
- import_metadata_core2.SysMetadataHistoryObject,
4948
+ import_metadata_core3.SysMetadataObject,
4949
+ import_metadata_core3.SysMetadataHistoryObject,
4523
4950
  // ADR-0067 commit log — sibling of sys_metadata_history (see note above).
4524
- import_metadata_core2.SysMetadataCommitObject,
4525
- import_metadata_core2.SysMetadataAuditObject,
4951
+ import_metadata_core3.SysMetadataCommitObject,
4952
+ import_metadata_core3.SysMetadataAuditObject,
4526
4953
  // Runtime view storage (shared / personal). Must always be provisioned so
4527
4954
  // end-user view creation via the generic data API has a place to write —
4528
4955
  // mirroring why sys_metadata is always provisioned for PUT /meta.
4529
- import_metadata_core2.SysViewDefinitionObject
4956
+ import_metadata_core3.SysViewDefinitionObject
4530
4957
  ];
4531
4958
  var REPO_SUBDIR = ".objectstack/metadata";
4532
4959
  var ARTIFACT_FIELD_TO_TYPE = {
@@ -4549,8 +4976,37 @@ var ARTIFACT_FIELD_TO_TYPE = {
4549
4976
  // positions from artifact ingestion.
4550
4977
  positions: "position",
4551
4978
  permissions: "permission",
4979
+ // [ADR-0066 D1] `capabilities` reaches the door at #12892 step 1, the
4980
+ // maintainer's `option 1` ruling ("the door owns the registration
4981
+ // route" for the five artifact security collections). Until #12894
4982
+ // measured it, `AppPlugin`'s `SECURITY_FIELDS` block
4983
+ // (packages/runtime/src/app-plugin.ts) was this collection's SOLE
4984
+ // registrar on an artifact boot — the one security collection the door
4985
+ // could not reach — so a declared capability was registered from bytes
4986
+ // nothing strict-parses, with no schema default and no ADR-0010
4987
+ // provenance. Measured on the two-reader harness, the door's copy adds
4988
+ // exactly four keys the raw copy lacks: `scope` (the schema default)
4989
+ // and `_packageId` / `_packageVersion` / `_provenance`.
4990
+ //
4991
+ // ⚠️ This entry makes the door a SECOND writer, not yet the only one:
4992
+ // `AppPlugin` still registers `capabilities`, and it runs last, so the
4993
+ // raw copy still wins a real artifact boot. Step 2 of the ruling (that
4994
+ // block stops registering these five on the artifact path, after a
4995
+ // census of the non-artifact boot paths) is what makes this the only
4996
+ // copy. Until then the divergence is the interim reality the ruling
4997
+ // explicitly permits, and #12878's pins are what keep it visible.
4998
+ capabilities: "capability",
4552
4999
  sharingRules: "sharing_rule",
4553
- policies: "policy",
5000
+ // `policies: 'policy'` removed at #12894: the stack schema is a
5001
+ // `strictObject` that declares no top-level `policies` key, so a
5002
+ // definition carrying one is refused by the strict parse a few lines
5003
+ // below — the entry could never match, and nothing was ever registered
5004
+ // under `policy` from this map. The word is real, but it lives ONE LEVEL
5005
+ // DOWN: on a permission set it is an alias for `rowLevelSecurity`
5006
+ // (`PERMISSION_SET_KEY_ALIASES`, packages/spec/src/security/permission.zod.ts)
5007
+ // — a key on an ITEM, never a collection. Third retirement of this exact
5008
+ // shape in this map (`themes` and `roles` above); the reasons are kept
5009
+ // in place because the first two are what made this one findable.
4554
5010
  apis: "api",
4555
5011
  webhooks: "webhook",
4556
5012
  agents: "agent",
@@ -4593,6 +5049,21 @@ var MetadataPlugin = class {
4593
5049
  * degrades on purpose (objects are discovered via the legacy fallback).
4594
5050
  */
4595
5051
  this.optionalDependencies = ["com.objectstack.engine.objectql"];
5052
+ /**
5053
+ * Once-per-process dedupe for the summaries the versioned artifact window
5054
+ * emits. The artifact watcher replays `_parseAndRegisterArtifact` on every
5055
+ * file change, so without this a dev loop over a legacy artifact would
5056
+ * re-announce the same finding on every reload — the same shape
5057
+ * `Protocol.storedConversionWarned` guards on the stored-row pass, which
5058
+ * this surfacing is modeled on.
5059
+ *
5060
+ * Two key families share the set, because they share the replay:
5061
+ * `<conversionId>|<label>` for a forward-conversion summary (#12772), and
5062
+ * `unbound-form-predicate-root|<label>` for the unbound-root notice
5063
+ * (#12915) — one line per artifact there, not one per conversion, since
5064
+ * the notice already aggregates every finding it made.
5065
+ */
5066
+ this.artifactConversionWarned = /* @__PURE__ */ new Set();
4596
5067
  this.init = async (ctx) => {
4597
5068
  this.initCtx = ctx;
4598
5069
  ctx.logger.info("Initializing Metadata Manager", {
@@ -4601,7 +5072,6 @@ var MetadataPlugin = class {
4601
5072
  artifactSource: this.options.artifactSource?.mode
4602
5073
  });
4603
5074
  ctx.registerService("metadata", this.manager);
4604
- console.log("[MetadataPlugin] Registered metadata service, has getRegisteredTypes:", typeof this.manager.getRegisteredTypes);
4605
5075
  const registerSysObjects = this.options.registerSystemObjects !== false;
4606
5076
  if (registerSysObjects) {
4607
5077
  try {
@@ -4623,7 +5093,7 @@ var MetadataPlugin = class {
4623
5093
  }
4624
5094
  ctx.logger.info("MetadataPlugin providing metadata service (primary mode)", {
4625
5095
  mode: this.options.artifactSource?.mode ?? "file-system",
4626
- features: ["watch", "multi-format", "query", "overlay", "type-registry"]
5096
+ features: ["watch", "multi-format", "query", "type-registry"]
4627
5097
  });
4628
5098
  };
4629
5099
  this.start = async (ctx) => {
@@ -4706,7 +5176,7 @@ var MetadataPlugin = class {
4706
5176
  if (httpServer && typeof httpServer.getRawApp === "function") {
4707
5177
  const { registerMetadataHmrRoutes: registerMetadataHmrRoutes2 } = await Promise.resolve().then(() => (init_hmr_routes(), hmr_routes_exports));
4708
5178
  const hub = registerMetadataHmrRoutes2(httpServer.getRawApp(), this.manager);
4709
- hub.setOnPostReload(async (body = {}) => {
5179
+ hub?.setOnPostReload(async (body = {}) => {
4710
5180
  const src3 = this.options.artifactSource;
4711
5181
  if (src3?.mode === "local-file") {
4712
5182
  try {
@@ -4746,7 +5216,7 @@ var MetadataPlugin = class {
4746
5216
  pending = true;
4747
5217
  try {
4748
5218
  await this._reloadAndAnnounce(ctx, src2, [src2.path]);
4749
- hub.broadcastReload("artifact-file-changed", [src2.path]);
5219
+ hub?.broadcastReload("artifact-file-changed", [src2.path]);
4750
5220
  ctx.logger.info("[MetadataPlugin] artifact auto-reloaded (file watcher)", {
4751
5221
  path: src2.path
4752
5222
  });
@@ -4768,7 +5238,13 @@ var MetadataPlugin = class {
4768
5238
  ctx.logger.warn("[MetadataPlugin] artifact watcher failed to start", { error: e?.message });
4769
5239
  }
4770
5240
  }
4771
- console.log("[MetadataPlugin] HMR endpoint registered at /api/v1/dev/metadata-events");
5241
+ if (hub) {
5242
+ console.log("[MetadataPlugin] HMR endpoint registered at /api/v1/dev/metadata-events");
5243
+ } else {
5244
+ console.log(
5245
+ `[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"})`
5246
+ );
5247
+ }
4772
5248
  } else {
4773
5249
  console.log("[MetadataPlugin] HTTP server with getRawApp() not available \u2014 skipping HMR endpoint");
4774
5250
  }
@@ -4864,6 +5340,106 @@ var MetadataPlugin = class {
4864
5340
  if (timer) clearTimeout(timer);
4865
5341
  }
4866
5342
  }
5343
+ /**
5344
+ * Versioned ADR-0087 forward conversion at the artifact-ingestion door
5345
+ * (#12772) — runs BEFORE the strict schema parse below, because the parse
5346
+ * is the refusal point.
5347
+ *
5348
+ * A compiled artifact is data at rest with a version stamp: built by
5349
+ * released tooling, then unchanged while the platform moves on. When a
5350
+ * spec release retires an authorable key inside a protocol line (spec
5351
+ * 17.1 → 17.2 retired the `allowRestore`/`allowPurge` permission bits),
5352
+ * every already-built artifact carrying the key becomes unbootable at the
5353
+ * tombstone — with no operator remedy, since `os migrate meta` targets
5354
+ * sources, not built artifacts. The stored-row read path already replays
5355
+ * the conversion chain for exactly this reason
5356
+ * (`applyConversionsToStoredItem`, ADR-0087 addendum); this is the same
5357
+ * policy at the artifact door, **keyed off the artifact's own declared
5358
+ * `engines.protocol` floor**: an artifact authored below the running spec
5359
+ * version converts forward, an artifact authored at the current (or a
5360
+ * newer) surface converts nothing and answers to the strict parse,
5361
+ * tombstones included. The version key is what keeps this a conversion
5362
+ * rather than an amnesty — the retired keys return with the M2 lifecycle
5363
+ * batch (#1883), and artifacts authored against that surface must never
5364
+ * have them stripped by history.
5365
+ *
5366
+ * Notices surface the way the stored-row pass's do — operator-visible and
5367
+ * deduped — as one summary line per conversion per artifact rather than
5368
+ * one per rewritten path (a real 17.1 artifact carried 150 strips of the
5369
+ * same two keys; 150 identical warn lines would bury the boot log).
5370
+ */
5371
+ _convertArtifactForward(ctx, definition, label) {
5372
+ const result = (0, import_metadata_core3.applyArtifactForwardConversions)(definition);
5373
+ this._warnUnboundFormPredicateRoots(ctx, result, label);
5374
+ if (result.notices.length === 0) return result.definition;
5375
+ const byConversion = /* @__PURE__ */ new Map();
5376
+ for (const n of result.notices) {
5377
+ const existing = byConversion.get(n.conversionId);
5378
+ if (existing) existing.count += 1;
5379
+ else byConversion.set(n.conversionId, { count: 1, firstPath: n.path, message: n.message });
5380
+ }
5381
+ for (const [conversionId, agg] of byConversion) {
5382
+ const key = `${conversionId}|${label}`;
5383
+ if (this.artifactConversionWarned.has(key)) continue;
5384
+ this.artifactConversionWarned.add(key);
5385
+ ctx.logger.warn(
5386
+ `[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.`
5387
+ );
5388
+ }
5389
+ return result.definition;
5390
+ }
5391
+ /**
5392
+ * Operator-facing boot notice for form-view predicates that fault OPEN on
5393
+ * this runtime (#12915 scope C — maintainer ruling 2026-08-28, 「同意C」).
5394
+ *
5395
+ * A form-view predicate binds `record` / `previous` / `parent` (runtime
5396
+ * record forms) or `data` (metadata-editing forms) — and a FIELD-level one
5397
+ * also binds `current_user` and its ADR-0068 aliases (objectui#6010),
5398
+ * which a SECTION-level one does not. The contract states beside that
5399
+ * vocabulary that a bare identifier is UNBOUND and the predicate faults,
5400
+ * and `visibleWhen`'s fault fallback is `true`. On a real
5401
+ * 17.1-built artifact that combination dead-ends record creation in the
5402
+ * console: the conditionally hidden field renders, and its unconditional
5403
+ * `required: true` — authored to be gated by the visibility that no longer
5404
+ * applies — blocks every submit, while the same payload POSTs 201 through
5405
+ * REST. Nothing refused, nothing logged, and only the operator can fix it
5406
+ * (by rebuilding the artifact), so this is the channel the ruling picked:
5407
+ * service startup, server-side, never a console surface — the person at
5408
+ * the form cannot act on "your artifact is stale".
5409
+ *
5410
+ * **Detection only.** No refusal, no rewrite, no behaviour change: the
5411
+ * predicate keeps faulting open exactly as before. Rewriting a bare root to
5412
+ * `record.` is the ADR-0087 conversion (#12915 scope A), deferred by the
5413
+ * same ruling with an explicit start line.
5414
+ *
5415
+ * **Same versioned window as the conversion replay above** — and read off
5416
+ * that pass's own verdict rather than recomputed, so the two can never
5417
+ * disagree about which artifacts are "old". An artifact declaring the
5418
+ * current (or a newer) floor answers to the strict parse and gets nothing
5419
+ * from here even when it does carry bare roots; that boundary is what keeps
5420
+ * a notice about legacy artifacts out of contract territory. An undeclared
5421
+ * range is treated as old data at rest, matching the grandfathering posture
5422
+ * the window already takes (`converted-undeclared`).
5423
+ */
5424
+ _warnUnboundFormPredicateRoots(ctx, result, label) {
5425
+ if (result.verdict !== "converted-forward" && result.verdict !== "converted-undeclared") return;
5426
+ const findings = (0, import_metadata_core3.detectUnboundFormViewPredicateRoots)(result.definition);
5427
+ if (findings.length === 0) return;
5428
+ const key = `unbound-form-predicate-root|${label}`;
5429
+ if (this.artifactConversionWarned.has(key)) return;
5430
+ this.artifactConversionWarned.add(key);
5431
+ const views = [...new Set(findings.map((f) => f.view))];
5432
+ const roots = [...new Set(findings.map((f) => f.root))];
5433
+ const quote = (list) => list.map((v) => `'${v}'`).join(", ");
5434
+ const surfaces = new Set(findings.map((f) => f.surface));
5435
+ const vocabulary = [
5436
+ surfaces.has("field") ? `on a form FIELD: ${quote(import_metadata_core3.BOUND_FORM_FIELD_PREDICATE_ROOTS)}` : null,
5437
+ surfaces.has("section") ? `on a form SECTION: ${quote(import_metadata_core3.BOUND_FORM_VIEW_PREDICATE_ROOTS)}` : null
5438
+ ].filter(Boolean).join("; ");
5439
+ ctx.logger.warn(
5440
+ `[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>').`
5441
+ );
5442
+ }
4867
5443
  /**
4868
5444
  * Parse raw artifact JSON (envelope or bare definition) and register all
4869
5445
  * metadata items into the MetadataManager.
@@ -4882,16 +5458,22 @@ var MetadataPlugin = class {
4882
5458
  let metadata;
4883
5459
  const obj = raw;
4884
5460
  if (obj?.schemaVersion && obj?.commitId && obj?.metadata !== void 0) {
4885
- const artifact = EnvironmentArtifactSchema.parse(obj);
5461
+ const artifact = EnvironmentArtifactSchema.parse({
5462
+ ...obj,
5463
+ metadata: this._convertArtifactForward(ctx, obj.metadata, label)
5464
+ });
4886
5465
  metadata = artifact.metadata;
4887
5466
  } else if (obj?.success && obj?.data?.metadata) {
4888
- const artifact = EnvironmentArtifactSchema.parse(obj.data);
5467
+ const artifact = EnvironmentArtifactSchema.parse({
5468
+ ...obj.data,
5469
+ metadata: this._convertArtifactForward(ctx, obj.data.metadata, label)
5470
+ });
4889
5471
  metadata = artifact.metadata;
4890
5472
  } else {
4891
- const def = ObjectStackDefinitionSchema.parse(obj);
5473
+ const def = ObjectStackDefinitionSchema.parse(this._convertArtifactForward(ctx, obj, label));
4892
5474
  const canonical = JSON.stringify(def, Object.keys(def).sort());
4893
5475
  const checksum = (0, import_node_crypto2.createHash)("sha256").update(canonical).digest("hex");
4894
- const environmentId = this.options.environmentId ?? "proj_local";
5476
+ const environmentId = this.options.environmentId ?? "env_local";
4895
5477
  EnvironmentArtifactSchema.parse({
4896
5478
  schemaVersion: "0.1",
4897
5479
  environmentId,
@@ -4905,53 +5487,127 @@ var MetadataPlugin = class {
4905
5487
  const memLoader = new MemoryLoader();
4906
5488
  const manifestPackageId = metadata?.manifest?.id ?? metadata?.id ?? void 0;
4907
5489
  const manifestVersion = metadata?.manifest?.version ?? metadata?.version ?? void 0;
5490
+ const carriesPackages = Array.isArray(metadata?.packages);
5491
+ const bodies = (0, import_core2.resolveArtifactPackageOrder)(metadata);
5492
+ const ownedByPackage = /* @__PURE__ */ new Map();
5493
+ const claim = (type, name) => {
5494
+ let names = ownedByPackage.get(type);
5495
+ if (!names) ownedByPackage.set(type, names = /* @__PURE__ */ new Set());
5496
+ names.add(name);
5497
+ };
5498
+ const claimed = (type, name) => ownedByPackage.get(type)?.has(name) === true;
5499
+ let totalRegistered = 0;
5500
+ for (const body of bodies) {
5501
+ totalRegistered += await this._registerArtifactBodyCollections(
5502
+ ctx,
5503
+ memLoader,
5504
+ body,
5505
+ carriesPackages ? {
5506
+ packageId: (0, import_core2.artifactPackageId)(body),
5507
+ packageVersion: body?.version ?? void 0
5508
+ } : { packageId: manifestPackageId, packageVersion: manifestVersion },
5509
+ { claim: carriesPackages ? claim : void 0 }
5510
+ );
5511
+ }
5512
+ if (carriesPackages) {
5513
+ const residual = await this._registerArtifactBodyCollections(
5514
+ ctx,
5515
+ memLoader,
5516
+ metadata,
5517
+ { packageId: manifestPackageId, packageVersion: manifestVersion },
5518
+ { skip: claimed }
5519
+ );
5520
+ totalRegistered += residual;
5521
+ if (residual > 0) {
5522
+ ctx.logger.warn(
5523
+ `[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.`
5524
+ );
5525
+ }
5526
+ }
5527
+ this.manager.registerLoader(memLoader);
5528
+ ctx.logger.info("[MetadataPlugin] Artifact metadata loaded", { source: label, totalRegistered });
5529
+ return totalRegistered;
5530
+ }
5531
+ /**
5532
+ * Register ONE artifact body's collections into the MetadataManager.
5533
+ *
5534
+ * A "body" is either the whole artifact (the single-package branch, where
5535
+ * the artifact and its one package are the same object) or one entry of
5536
+ * `packages[]` (ADR-0130 D4), which is an assembled
5537
+ * `{ ...manifest, ...collections }` payload carrying the same collection
5538
+ * keys the top level does. The loop is identical for both — that is the
5539
+ * point: there is one ingestion of a collection here, not one per shape.
5540
+ *
5541
+ * @param provenance - The `(packageId, packageVersion)` every item found in
5542
+ * this body is stamped with (ADR-0010 §3.7, via `applyProtection`). It is
5543
+ * the body's OWN identity, never the enclosing artifact's, which is what
5544
+ * makes a multi-package artifact's items agree with the registry and with
5545
+ * `GET /api/v1/packages` about who owns them.
5546
+ * @param slots.claim - Called with every `(type, name)` this pass
5547
+ * registered. Passed when reading package bodies; the residual sweep uses
5548
+ * what it recorded.
5549
+ * @param slots.skip - Consulted before registering each `(type, name)`.
5550
+ * Passed ONLY by the residual sweep, so a package body's copy is never
5551
+ * overwritten by the flattened top-level copy of the same definition —
5552
+ * the overwrite that re-attributed the item to the artifact's manifest.
5553
+ * ⛔ It is never passed while reading the bodies themselves: two items of
5554
+ * one name inside one body still register as they always have (last
5555
+ * wins), because suppressing that would be a behaviour change on the
5556
+ * single-package branch D7 pins.
5557
+ * @returns How many items this body registered.
5558
+ */
5559
+ async _registerArtifactBodyCollections(ctx, memLoader, body, provenance, slots = {}) {
5560
+ const { packageId, packageVersion } = provenance;
4908
5561
  let totalRegistered = 0;
4909
5562
  for (const [field, metaType] of Object.entries(ARTIFACT_FIELD_TO_TYPE)) {
4910
- const items = metadata[field];
5563
+ const items = body[field];
4911
5564
  if (!Array.isArray(items) || items.length === 0) continue;
4912
5565
  for (const item of items) {
4913
- if (metaType === "view" && (0, import_spec3.isAggregatedViewContainer)(item)) {
4914
- const viewObject = item?.list?.data?.object ?? item?.form?.data?.object;
5566
+ if (metaType === "view" && (0, import_spec4.isAggregatedViewContainer)(item)) {
5567
+ const viewObject = deriveViewContainerObject(item);
4915
5568
  if (!viewObject) continue;
4916
- (0, import_shared2.applyProtection)(item, {
4917
- packageId: manifestPackageId,
4918
- packageVersion: manifestVersion
5569
+ if (slots.skip?.("view", viewObject)) continue;
5570
+ (0, import_shared3.applyProtection)(item, {
5571
+ packageId,
5572
+ packageVersion
4919
5573
  });
4920
5574
  await memLoader.save("view", viewObject, item);
4921
5575
  await this.manager.register("view", viewObject, item, { notify: false });
4922
5576
  totalRegistered++;
4923
- for (const vi of (0, import_spec3.expandViewContainer)(viewObject, item)) {
5577
+ slots.claim?.("view", viewObject);
5578
+ for (const vi of (0, import_spec4.expandViewContainer)(viewObject, item)) {
4924
5579
  for (const w of vi._diagnostics?.warnings ?? []) {
4925
5580
  ctx.logger.warn(`[MetadataPlugin] View expansion warning for '${vi.name}': ${w.message}`);
4926
5581
  }
4927
- (0, import_shared2.applyProtection)(vi, {
4928
- packageId: manifestPackageId,
4929
- packageVersion: manifestVersion
5582
+ (0, import_shared3.applyProtection)(vi, {
5583
+ packageId,
5584
+ packageVersion
4930
5585
  });
4931
5586
  await memLoader.save("view", vi.name, vi);
4932
5587
  await this.manager.register("view", vi.name, vi, { notify: false });
4933
5588
  totalRegistered++;
5589
+ slots.claim?.("view", vi.name);
4934
5590
  }
4935
5591
  continue;
4936
5592
  }
4937
5593
  let name = item?.name;
4938
5594
  if (!name) {
4939
5595
  if (metaType === "view") {
4940
- name = item?.list?.data?.object ?? item?.form?.data?.object;
5596
+ name = deriveViewContainerObject(item);
4941
5597
  }
4942
5598
  }
4943
5599
  if (!name) continue;
4944
- (0, import_shared2.applyProtection)(item, {
4945
- packageId: manifestPackageId,
4946
- packageVersion: manifestVersion
5600
+ if (slots.skip?.(metaType, name)) continue;
5601
+ (0, import_shared3.applyProtection)(item, {
5602
+ packageId,
5603
+ packageVersion
4947
5604
  });
4948
5605
  await memLoader.save(metaType, name, item);
4949
5606
  await this.manager.register(metaType, name, item, { notify: false });
4950
5607
  totalRegistered++;
5608
+ slots.claim?.(metaType, name);
4951
5609
  }
4952
5610
  }
4953
- this.manager.registerLoader(memLoader);
4954
- ctx.logger.info("[MetadataPlugin] Artifact metadata loaded", { source: label, totalRegistered });
4955
5611
  return totalRegistered;
4956
5612
  }
4957
5613
  /**
@@ -5027,7 +5683,7 @@ var MetadataPlugin = class {
5027
5683
  for (const item of items) {
5028
5684
  const meta = item;
5029
5685
  if (meta?.name) {
5030
- (0, import_shared2.applyProtection)(meta, {
5686
+ (0, import_shared3.applyProtection)(meta, {
5031
5687
  packageId: this.options.packageId
5032
5688
  });
5033
5689
  await this.manager.register(entry.type, meta.name, item, { notify: false });
@@ -5123,9 +5779,43 @@ var RemoteLoader = class {
5123
5779
  format: "json"
5124
5780
  };
5125
5781
  }
5782
+ /**
5783
+ * [#15037] Report only the names that ARE names.
5784
+ *
5785
+ * This read used to be `loadMany<{ name: string }>(type)` mapped straight to
5786
+ * `items.map(i => i.name)`. That type argument is an ASSERTION about bodies
5787
+ * that arrived over HTTP, and nothing checked it: a body with no top-level
5788
+ * `name` yielded `undefined`, which went into an array this signature
5789
+ * declares as `string[]` and reached consumers through
5790
+ * `MetadataManager.listNames()` — a runtime violation of a declared type,
5791
+ * not an untidy entry. A consumer that keys by it, lower-cases it, or feeds
5792
+ * it back to a by-name `load()` gets `undefined` where the type says it
5793
+ * cannot be.
5794
+ *
5795
+ * The guard is `DatabaseLoader.list()`'s, one file away: same cast-then-map
5796
+ * spelling, one `typeof` filter behind it. Silently dropping is the landed
5797
+ * direction, not a preference — `DatabaseLoader` drops rather than throws,
5798
+ * and `FilesystemLoader`'s narrowing carries a maintainer ruling (via the
5799
+ * director seat on #14486, 2026-09-02) that chose narrowing (A) over
5800
+ * refusing loudly (B), because a name in the list that the door answers
5801
+ * `null` for is the silent failure an author reads as their own typo. An
5802
+ * `undefined` here is the extreme form of that name.
5803
+ *
5804
+ * ⛔ NOT copied from the siblings: `MemoryLoader` answers with its store
5805
+ * keys, and #14205 ruled that identity is the key the store holds an item
5806
+ * under rather than `body.name`. This loader reads over HTTP and holds no
5807
+ * store key, so `body.name` is the only identity it has — the list is
5808
+ * narrowed to agree with the door instead. `loadMany()` is deliberately
5809
+ * untouched: it keys nothing, so a nameless body is still served there.
5810
+ *
5811
+ * The predicate is spelled as a type guard, and the mapped element type left
5812
+ * `unknown`, so `tsc` PROVES the declared `string[]` instead of a cast
5813
+ * asserting it — otherwise the compiler reads the filter as always-true and
5814
+ * a later reader deletes it as dead.
5815
+ */
5126
5816
  async list(type) {
5127
5817
  const items = await this.loadMany(type);
5128
- return items.map((i) => i.name);
5818
+ return items.map((item) => item.name).filter((name) => typeof name === "string");
5129
5819
  }
5130
5820
  async save(type, name, data, _options) {
5131
5821
  const response = await fetch(`${this.baseUrl}/${type}/${name}`, {
@@ -5145,7 +5835,7 @@ var RemoteLoader = class {
5145
5835
  };
5146
5836
 
5147
5837
  // src/index.ts
5148
- var import_metadata_core3 = require("@objectstack/metadata-core");
5838
+ var import_metadata_core4 = require("@objectstack/metadata-core");
5149
5839
 
5150
5840
  // src/utils/history-cleanup.ts
5151
5841
  var import_kernel3 = require("@objectstack/spec/kernel");
@@ -5165,9 +5855,9 @@ var HistoryCleanupManager = class {
5165
5855
  return;
5166
5856
  }
5167
5857
  const intervalMs = (this.policy.cleanupIntervalHours ?? 24) * 60 * 60 * 1e3;
5168
- void this.runCleanup();
5858
+ void runCleanupAndReport(this);
5169
5859
  this.cleanupTimer = setInterval(() => {
5170
- void this.runCleanup();
5860
+ void runCleanupAndReport(this);
5171
5861
  }, intervalMs);
5172
5862
  }
5173
5863
  /**
@@ -5194,7 +5884,7 @@ var HistoryCleanupManager = class {
5194
5884
  try {
5195
5885
  if (this.policy.maxAgeDays) {
5196
5886
  const cutoffDate = /* @__PURE__ */ new Date();
5197
- cutoffDate.setDate(cutoffDate.getDate() - this.policy.maxAgeDays);
5887
+ cutoffDate.setUTCDate(cutoffDate.getUTCDate() - this.policy.maxAgeDays);
5198
5888
  const cutoffISO = cutoffDate.toISOString();
5199
5889
  const filter = {
5200
5890
  recorded_at: { $lt: cutoffISO }
@@ -5314,7 +6004,7 @@ var HistoryCleanupManager = class {
5314
6004
  if (organizationId) baseWhere.organization_id = organizationId;
5315
6005
  if (this.policy.maxAgeDays) {
5316
6006
  const cutoffDate = /* @__PURE__ */ new Date();
5317
- cutoffDate.setDate(cutoffDate.getDate() - this.policy.maxAgeDays);
6007
+ cutoffDate.setUTCDate(cutoffDate.getUTCDate() - this.policy.maxAgeDays);
5318
6008
  const cutoffISO = cutoffDate.toISOString();
5319
6009
  const filter = {
5320
6010
  recorded_at: { $lt: cutoffISO },
@@ -5361,6 +6051,23 @@ var HistoryCleanupManager = class {
5361
6051
  };
5362
6052
  }
5363
6053
  };
6054
+ async function runCleanupAndReport(manager) {
6055
+ let outcome;
6056
+ try {
6057
+ outcome = await manager.runCleanup();
6058
+ } catch (error) {
6059
+ console.error(
6060
+ "History cleanup: the run did not complete, so no history row past the retention policy was deleted and the table keeps growing while the system reports healthy. Fix: the cause below comes from the configured data driver, not from the retention policy; call `runCleanup()` directly to reproduce it. Cause:",
6061
+ error
6062
+ );
6063
+ return;
6064
+ }
6065
+ if (outcome.errors > 0) {
6066
+ console.error(
6067
+ `History cleanup: ${outcome.errors} delete operation(s) failed and ${outcome.deleted} row(s) were deleted. The history rows those deletes were meant to remove are still in the table, nothing retries them, and the table grows past the retention policy while the system keeps reporting healthy. Fix: check the data driver delete path for the metadata history table. The per-failure causes are not carried out of \`runCleanup()\`, so reproduce them against the driver directly.`
6068
+ );
6069
+ }
6070
+ }
5364
6071
 
5365
6072
  // src/migration/index.ts
5366
6073
  var migration_exports = {};
@@ -5419,6 +6126,9 @@ var MigrationExecutor = class {
5419
6126
  };
5420
6127
  // Annotate the CommonJS export names for ESM import in node:
5421
6128
  0 && (module.exports = {
6129
+ AMBIGUOUS_METADATA_STEM_CODE,
6130
+ AMBIGUOUS_METADATA_STEM_STATUS,
6131
+ AmbiguousMetadataStemError,
5422
6132
  DatabaseLoader,
5423
6133
  FilesystemLoader,
5424
6134
  HistoryCleanupManager,
@@ -5434,7 +6144,9 @@ var MigrationExecutor = class {
5434
6144
  TypeScriptSerializer,
5435
6145
  YAMLSerializer,
5436
6146
  calculateChecksum,
6147
+ deriveViewContainerObject,
5437
6148
  generateDiffSummary,
5438
- generateSimpleDiff
6149
+ generateSimpleDiff,
6150
+ isAmbiguousMetadataStemError
5439
6151
  });
5440
6152
  //# sourceMappingURL=node.cjs.map