@objectstack/metadata 17.3.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
@@ -197,6 +197,9 @@ var init_hmr_routes = __esm({
197
197
  // src/index.ts
198
198
  var index_exports = {};
199
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,
200
203
  DatabaseLoader: () => DatabaseLoader,
201
204
  HistoryCleanupManager: () => HistoryCleanupManager,
202
205
  JSONSerializer: () => JSONSerializer,
@@ -212,7 +215,8 @@ __export(index_exports, {
212
215
  calculateChecksum: () => calculateChecksum,
213
216
  deriveViewContainerObject: () => deriveViewContainerObject,
214
217
  generateDiffSummary: () => generateDiffSummary,
215
- generateSimpleDiff: () => generateSimpleDiff
218
+ generateSimpleDiff: () => generateSimpleDiff,
219
+ isAmbiguousMetadataStemError: () => isAmbiguousMetadataStemError
216
220
  });
217
221
  module.exports = __toCommonJS(index_exports);
218
222
 
@@ -640,7 +644,7 @@ async function _columnExists(exec, table, column) {
640
644
  // src/loaders/database-loader.ts
641
645
  function canonicalIsoInstant(value) {
642
646
  if (value === null || value === void 0) return void 0;
643
- if (value instanceof Date) return value.toISOString();
647
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? void 0 : value.toISOString();
644
648
  if (typeof value === "string") return value;
645
649
  return String(value);
646
650
  }
@@ -696,7 +700,7 @@ var DatabaseLoader = class {
696
700
  if (cacheEnabled) {
697
701
  const lruOpts = {
698
702
  maxSize: cacheOpts?.maxSize ?? 500,
699
- ttl: cacheOpts?.ttl ?? 6e4
703
+ ttl: cacheOpts?.ttlMs ?? 6e4
700
704
  };
701
705
  this.loadCache = new LRUCache(lruOpts);
702
706
  this.loadManyCache = new LRUCache(lruOpts);
@@ -1546,6 +1550,33 @@ function generateId() {
1546
1550
  return `meta_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
1547
1551
  }
1548
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
+
1549
1580
  // src/endpoint-matcher.ts
1550
1581
  var import_api = require("@objectstack/spec/api");
1551
1582
 
@@ -1696,6 +1727,8 @@ var EndpointMatcher = class {
1696
1727
  // src/view-container-expansion.ts
1697
1728
  var import_spec2 = require("@objectstack/spec");
1698
1729
  var import_shared2 = require("@objectstack/spec/shared");
1730
+
1731
+ // src/view-container.ts
1699
1732
  function deriveViewContainerObject(container) {
1700
1733
  if (!container || typeof container !== "object") return void 0;
1701
1734
  const c = container;
@@ -1703,6 +1736,8 @@ function deriveViewContainerObject(container) {
1703
1736
  const byName = typeof c.name === "string" && c.name ? c.name : void 0;
1704
1737
  return own ?? c?.list?.data?.object ?? c?.form?.data?.object ?? byName;
1705
1738
  }
1739
+
1740
+ // src/view-container-expansion.ts
1706
1741
  function expandRuntimeViewContainer(data) {
1707
1742
  if (!(0, import_spec2.isAggregatedViewContainer)(data)) return [];
1708
1743
  const container = data;
@@ -2408,6 +2443,9 @@ var _MetadataManager = class _MetadataManager {
2408
2443
  await this.admitLoaderItems(loader, type, items);
2409
2444
  this.reportLoaderReadRecovered(loader.contract.name);
2410
2445
  } catch (e) {
2446
+ if (isAmbiguousMetadataStemError(e)) {
2447
+ throw e;
2448
+ }
2411
2449
  degraded = true;
2412
2450
  errors.push(`${loader.contract.name}: ${e instanceof Error ? e.message : String(e)}`);
2413
2451
  this.reportLoaderReadFailure(loader.contract.name, type, e);
@@ -2745,6 +2783,30 @@ var _MetadataManager = class _MetadataManager {
2745
2783
  }
2746
2784
  /**
2747
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.
2748
2810
  */
2749
2811
  async listNames(type) {
2750
2812
  type = (0, import_core.canonicalMetadataServiceType)(type);
@@ -2756,8 +2818,16 @@ var _MetadataManager = class _MetadataManager {
2756
2818
  }
2757
2819
  }
2758
2820
  for (const loader of this.loaders.values()) {
2759
- const result = await loader.list(type);
2760
- 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
+ }
2761
2831
  }
2762
2832
  return Array.from(names);
2763
2833
  }
@@ -3617,6 +3687,99 @@ var _MetadataManager = class _MetadataManager {
3617
3687
  }
3618
3688
  return results;
3619
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
+ }
3620
3783
  /**
3621
3784
  * Save metadata item to a loader
3622
3785
  */
@@ -4249,33 +4412,34 @@ var _FilesystemLoader = class _FilesystemLoader {
4249
4412
  const globPatterns = patterns.map(
4250
4413
  (pattern) => path.join(typeDir, pattern)
4251
4414
  );
4415
+ const files = [];
4252
4416
  for (const pattern of globPatterns) {
4253
- const files = await (0, import_glob.glob)(pattern, {
4254
- ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*", "**/*[*]*"],
4255
- nodir: true
4256
- });
4257
- for (const file of files) {
4258
- if (limit && items.length >= limit) {
4259
- break;
4260
- }
4261
- try {
4262
- const content = await fs.readFile(file, "utf-8");
4263
- const format = this.detectFormat(file);
4264
- const serializer = this.getSerializer(format);
4265
- if (serializer) {
4266
- const data = serializer.deserialize(content);
4267
- items.push({ file, data });
4268
- }
4269
- } catch (error) {
4270
- this.logger?.warn("Failed to load file", {
4271
- file,
4272
- error: error instanceof Error ? error.message : String(error)
4273
- });
4274
- }
4275
- }
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) {
4276
4426
  if (limit && items.length >= limit) {
4277
4427
  break;
4278
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
+ }
4279
4443
  }
4280
4444
  return items;
4281
4445
  } catch (error) {
@@ -4344,13 +4508,13 @@ var _FilesystemLoader = class _FilesystemLoader {
4344
4508
  */
4345
4509
  async list(type) {
4346
4510
  const typeDir = path.join(this.rootDir, type);
4511
+ let files;
4347
4512
  try {
4348
- const files = await (0, import_glob.glob)("**/*", {
4513
+ files = await (0, import_glob.glob)("**/*", {
4349
4514
  cwd: typeDir,
4350
4515
  ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*"],
4351
4516
  nodir: true
4352
4517
  });
4353
- return files.map((file) => this.resolvableNameForPath(typeDir, path.join(typeDir, file))).filter((name) => name !== null);
4354
4518
  } catch (error) {
4355
4519
  this.logger?.error("Failed to list", void 0, {
4356
4520
  type,
@@ -4358,6 +4522,7 @@ var _FilesystemLoader = class _FilesystemLoader {
4358
4522
  });
4359
4523
  return [];
4360
4524
  }
4525
+ return this.resolvableNames(type, typeDir, files.map((file) => path.join(typeDir, file)));
4361
4526
  }
4362
4527
  async save(type, name, data, options) {
4363
4528
  const startTime = Date.now();
@@ -4482,6 +4647,49 @@ var _FilesystemLoader = class _FilesystemLoader {
4482
4647
  }
4483
4648
  return _FilesystemLoader.nameFromFilename(rel);
4484
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
+ }
4485
4693
  /**
4486
4694
  * Find file for a given type and name
4487
4695
  */
@@ -5569,9 +5777,43 @@ var RemoteLoader = class {
5569
5777
  format: "json"
5570
5778
  };
5571
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
+ */
5572
5814
  async list(type) {
5573
5815
  const items = await this.loadMany(type);
5574
- return items.map((i) => i.name);
5816
+ return items.map((item) => item.name).filter((name) => typeof name === "string");
5575
5817
  }
5576
5818
  async save(type, name, data, _options) {
5577
5819
  const response = await fetch(`${this.baseUrl}/${type}/${name}`, {
@@ -5611,9 +5853,9 @@ var HistoryCleanupManager = class {
5611
5853
  return;
5612
5854
  }
5613
5855
  const intervalMs = (this.policy.cleanupIntervalHours ?? 24) * 60 * 60 * 1e3;
5614
- void this.runCleanup();
5856
+ void runCleanupAndReport(this);
5615
5857
  this.cleanupTimer = setInterval(() => {
5616
- void this.runCleanup();
5858
+ void runCleanupAndReport(this);
5617
5859
  }, intervalMs);
5618
5860
  }
5619
5861
  /**
@@ -5640,7 +5882,7 @@ var HistoryCleanupManager = class {
5640
5882
  try {
5641
5883
  if (this.policy.maxAgeDays) {
5642
5884
  const cutoffDate = /* @__PURE__ */ new Date();
5643
- cutoffDate.setDate(cutoffDate.getDate() - this.policy.maxAgeDays);
5885
+ cutoffDate.setUTCDate(cutoffDate.getUTCDate() - this.policy.maxAgeDays);
5644
5886
  const cutoffISO = cutoffDate.toISOString();
5645
5887
  const filter = {
5646
5888
  recorded_at: { $lt: cutoffISO }
@@ -5760,7 +6002,7 @@ var HistoryCleanupManager = class {
5760
6002
  if (organizationId) baseWhere.organization_id = organizationId;
5761
6003
  if (this.policy.maxAgeDays) {
5762
6004
  const cutoffDate = /* @__PURE__ */ new Date();
5763
- cutoffDate.setDate(cutoffDate.getDate() - this.policy.maxAgeDays);
6005
+ cutoffDate.setUTCDate(cutoffDate.getUTCDate() - this.policy.maxAgeDays);
5764
6006
  const cutoffISO = cutoffDate.toISOString();
5765
6007
  const filter = {
5766
6008
  recorded_at: { $lt: cutoffISO },
@@ -5807,6 +6049,23 @@ var HistoryCleanupManager = class {
5807
6049
  };
5808
6050
  }
5809
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
+ }
5810
6069
 
5811
6070
  // src/migration/index.ts
5812
6071
  var migration_exports = {};
@@ -5865,6 +6124,9 @@ var MigrationExecutor = class {
5865
6124
  };
5866
6125
  // Annotate the CommonJS export names for ESM import in node:
5867
6126
  0 && (module.exports = {
6127
+ AMBIGUOUS_METADATA_STEM_CODE,
6128
+ AMBIGUOUS_METADATA_STEM_STATUS,
6129
+ AmbiguousMetadataStemError,
5868
6130
  DatabaseLoader,
5869
6131
  HistoryCleanupManager,
5870
6132
  JSONSerializer,
@@ -5880,6 +6142,7 @@ var MigrationExecutor = class {
5880
6142
  calculateChecksum,
5881
6143
  deriveViewContainerObject,
5882
6144
  generateDiffSummary,
5883
- generateSimpleDiff
6145
+ generateSimpleDiff,
6146
+ isAmbiguousMetadataStemError
5884
6147
  });
5885
6148
  //# sourceMappingURL=index.cjs.map