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