@remnic/core 10.0.55 → 10.0.56

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.
Files changed (35) hide show
  1. package/dist/access-cli.js +8 -8
  2. package/dist/access-http-query.js +2 -2
  3. package/dist/access-http.js +3 -3
  4. package/dist/{chunk-PVG7TCUG.js → chunk-5C44AMRV.js} +2 -2
  5. package/dist/{chunk-2GU6XS4L.js → chunk-CC2ESKHP.js} +7 -7
  6. package/dist/{chunk-R3WZ74B2.js → chunk-CZS7UTXL.js} +5 -3
  7. package/dist/chunk-CZS7UTXL.js.map +1 -0
  8. package/dist/{chunk-4BUJICMI.js → chunk-FCAXGYDT.js} +2 -2
  9. package/dist/{chunk-GGFQAGOT.js → chunk-GMRP3HN3.js} +2 -2
  10. package/dist/{chunk-Y56D5FSD.js → chunk-OGR52N7A.js} +3 -3
  11. package/dist/{chunk-XVTB6BZS.js → chunk-S3OI5DP5.js} +22 -10
  12. package/dist/chunk-S3OI5DP5.js.map +1 -0
  13. package/dist/{chunk-WGFEGBFB.js → chunk-VZ7VJGPL.js} +2 -2
  14. package/dist/cli.js +6 -6
  15. package/dist/index.js +8 -8
  16. package/dist/maintenance/namespace-planner.d.ts.map +1 -1
  17. package/dist/namespaces/catalog.d.ts +9 -1
  18. package/dist/namespaces/catalog.d.ts.map +1 -1
  19. package/dist/operator-toolkit.js +3 -3
  20. package/dist/orchestration/maintenance.js +3 -3
  21. package/dist/orchestrator.js +8 -8
  22. package/package.json +2 -2
  23. package/src/maintenance/namespace-planner.test.ts +41 -1
  24. package/src/maintenance/namespace-planner.ts +8 -1
  25. package/src/namespaces/catalog.test.ts +83 -0
  26. package/src/namespaces/catalog.ts +46 -10
  27. package/src/orchestration/maintenance.test.ts +33 -1
  28. package/dist/chunk-R3WZ74B2.js.map +0 -1
  29. package/dist/chunk-XVTB6BZS.js.map +0 -1
  30. /package/dist/{chunk-PVG7TCUG.js.map → chunk-5C44AMRV.js.map} +0 -0
  31. /package/dist/{chunk-2GU6XS4L.js.map → chunk-CC2ESKHP.js.map} +0 -0
  32. /package/dist/{chunk-4BUJICMI.js.map → chunk-FCAXGYDT.js.map} +0 -0
  33. /package/dist/{chunk-GGFQAGOT.js.map → chunk-GMRP3HN3.js.map} +0 -0
  34. /package/dist/{chunk-Y56D5FSD.js.map → chunk-OGR52N7A.js.map} +0 -0
  35. /package/dist/{chunk-WGFEGBFB.js.map → chunk-VZ7VJGPL.js.map} +0 -0
@@ -4,13 +4,13 @@ import {
4
4
  graphPathRelativeToStorage,
5
5
  mergeGraphExpandedResults,
6
6
  qmdStartupCollectionChecksWithTimeout
7
- } from "./chunk-2GU6XS4L.js";
7
+ } from "./chunk-CC2ESKHP.js";
8
8
  import "./chunk-YDIOMDBW.js";
9
9
  import "./chunk-HM3ZTDAX.js";
10
10
  import {
11
11
  formatCompressionGuidelinesForRecall
12
12
  } from "./chunk-5PHOU5QI.js";
13
- import "./chunk-PVG7TCUG.js";
13
+ import "./chunk-5C44AMRV.js";
14
14
  import "./chunk-I74SUMNI.js";
15
15
  import "./chunk-ILNPU5W5.js";
16
16
  import "./chunk-VZAKYDMJ.js";
@@ -121,7 +121,7 @@ import "./chunk-PYIR4SSB.js";
121
121
  import "./chunk-7G5NTS7F.js";
122
122
  import "./chunk-FFQNNFHX.js";
123
123
  import "./chunk-3I4FUAVJ.js";
124
- import "./chunk-Y56D5FSD.js";
124
+ import "./chunk-OGR52N7A.js";
125
125
  import "./chunk-MMACM5G2.js";
126
126
  import "./chunk-54BGIBCR.js";
127
127
  import "./chunk-H4CKS5B2.js";
@@ -164,8 +164,8 @@ import "./chunk-HDN7IVIO.js";
164
164
  import "./chunk-LR7RW4T6.js";
165
165
  import "./chunk-3T74IZB3.js";
166
166
  import "./chunk-2LA3H54D.js";
167
- import "./chunk-GGFQAGOT.js";
168
- import "./chunk-R3WZ74B2.js";
167
+ import "./chunk-GMRP3HN3.js";
168
+ import "./chunk-CZS7UTXL.js";
169
169
  import "./chunk-KFY3SGN7.js";
170
170
  import "./chunk-CMTINOFS.js";
171
171
  import "./chunk-M4U25HLR.js";
@@ -238,7 +238,7 @@ import "./chunk-27NO2KVB.js";
238
238
  import "./chunk-KFZY2OFH.js";
239
239
  import "./chunk-B32WYRXX.js";
240
240
  import "./chunk-QW27DOHG.js";
241
- import "./chunk-4BUJICMI.js";
241
+ import "./chunk-FCAXGYDT.js";
242
242
  import "./chunk-V7IZKJI3.js";
243
243
  import "./chunk-TB6CBZ5V.js";
244
244
  import "./chunk-XW2T4QAW.js";
@@ -283,8 +283,8 @@ import "./chunk-2FLFJTCI.js";
283
283
  import "./chunk-JATHBCAV.js";
284
284
  import "./chunk-ZQUVBZI4.js";
285
285
  import "./chunk-5R627MYY.js";
286
- import "./chunk-WGFEGBFB.js";
287
- import "./chunk-XVTB6BZS.js";
286
+ import "./chunk-VZ7VJGPL.js";
287
+ import "./chunk-S3OI5DP5.js";
288
288
  import "./chunk-AB3A7EFH.js";
289
289
  import "./chunk-KIUVEMBI.js";
290
290
  import "./chunk-HJ4X4BGS.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "10.0.55",
3
+ "version": "10.0.56",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3346,7 +3346,7 @@
3346
3346
  "core"
3347
3347
  ],
3348
3348
  "peerDependencies": {
3349
- "@remnic/coding-graph": "^10.0.55"
3349
+ "@remnic/coding-graph": "^10.0.56"
3350
3350
  },
3351
3351
  "peerDependenciesMeta": {
3352
3352
  "@remnic/coding-graph": {
@@ -4,7 +4,7 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import test from "node:test";
6
6
 
7
- import type { NamespaceCatalog, NamespaceRecord } from "../namespaces/catalog.js";
7
+ import type { NamespaceCatalog, NamespaceCatalogFilter, NamespaceCatalogReadOptions, NamespaceRecord } from "../namespaces/catalog.js";
8
8
  import { namespaceIdentityToken } from "../namespaces/identity.js";
9
9
  import type { PluginConfig } from "../types.js";
10
10
  import {
@@ -145,6 +145,46 @@ test("planner fans out to live cataloged project and team namespaces while skipp
145
145
  }
146
146
  });
147
147
 
148
+ test("planner records catalog_read_failed for an incomplete catalog read and still plans what it found (#3293)", async () => {
149
+ const memoryDir = await mkMemoryDir();
150
+ try {
151
+ await createNamespaceData(memoryDir, "project-alpha");
152
+ // Object.create(null) yields an untyped value, so no assertion is needed: the planner only
153
+ // reads enabled and calls listNamespaces.
154
+ const catalog: NamespaceCatalog = Object.assign(Object.create(null), {
155
+ enabled: true,
156
+ async listNamespaces(_filter?: NamespaceCatalogFilter, options?: NamespaceCatalogReadOptions) {
157
+ options?.onIncomplete?.("1 malformed catalog line skipped");
158
+ return [record(memoryDir, "project-alpha", "project", "2026-06-30T10:00:00.000Z")];
159
+ },
160
+ });
161
+
162
+ const plan = await planNamespaceMaintenance(makeConfig(memoryDir), { jobName: "qmd", catalog });
163
+
164
+ assert.ok(
165
+ plan.namespaces.some((candidate) => candidate.namespace === "project-alpha"),
166
+ "the namespaces that were read are still maintained"
167
+ );
168
+ const failed = plan.skipped.filter((skipped) => skipped.reason === "catalog_read_failed");
169
+ assert.equal(failed.length, 1);
170
+ assert.equal(failed[0].namespace, "*");
171
+ assert.match(failed[0].detail ?? "", /malformed/);
172
+ } finally {
173
+ await rm(memoryDir, { recursive: true, force: true });
174
+ }
175
+ });
176
+
177
+ test("planner without a catalog plans the configured namespaces and reports nothing (#3293)", async () => {
178
+ const memoryDir = await mkMemoryDir();
179
+ try {
180
+ const plan = await planNamespaceMaintenance(makeConfig(memoryDir), { jobName: "qmd" });
181
+ assert.deepEqual(plan.namespaces.map((candidate) => candidate.namespace), ["default", "shared"]);
182
+ assert.equal(plan.skipped.some((skipped) => skipped.reason === "catalog_read_failed"), false);
183
+ } finally {
184
+ await rm(memoryDir, { recursive: true, force: true });
185
+ }
186
+ });
187
+
148
188
  test("planner includes branch namespaces only when branch maintenance is enabled", async () => {
149
189
  const memoryDir = await mkMemoryDir();
150
190
  try {
@@ -229,7 +229,14 @@ export async function planNamespaceMaintenance(
229
229
  if (resolveNamespaceCapabilities(config).namespaces && resolveConversationContextCapabilities(config).maintenanceNamespaceFanout !== false) {
230
230
  const configuredSet = new Set(configured);
231
231
  try {
232
- const records = options.catalog?.enabled ? await options.catalog.listNamespaces() : [];
232
+ // An incomplete read (unreadable file, malformed rows) still returns the readable records,
233
+ // so they are planned as before; the callback records that the catalog was not whole.
234
+ const records = options.catalog?.enabled
235
+ ? await options.catalog.listNamespaces(undefined, {
236
+ onIncomplete: (reason) =>
237
+ skipped.push({ namespace: "*", reason: "catalog_read_failed", detail: reason }),
238
+ })
239
+ : [];
233
240
  for (const record of records) {
234
241
  const namespace = record.namespace.trim();
235
242
  if (!namespace) continue;
@@ -3997,3 +3997,86 @@ test("#2469 persisted self/legacy kinds coerce to explicit and drop unused hints
3997
3997
  }
3998
3998
  });
3999
3999
 
4000
+
4001
+ test("listNamespaces reports an incomplete read for a malformed record but still returns the valid ones (#3293)", async () => {
4002
+ const memoryDir = await mkMemoryDir();
4003
+ try {
4004
+ const catalog = new NamespaceCatalog(makeConfig(memoryDir));
4005
+ await catalog.markWrite("project-origin-abc123", { discoveredBy: "write" });
4006
+ const catalogPath = path.join(memoryDir, "state", "namespaces.jsonl");
4007
+ await appendFile(catalogPath, "{this is not json\n", "utf8");
4008
+
4009
+ const reasons: string[] = [];
4010
+ const records = await catalog.listNamespaces(undefined, { onIncomplete: (reason) => reasons.push(reason) });
4011
+ assert.deepEqual(records.map((r) => r.namespace), ["project-origin-abc123"], "valid records are still returned");
4012
+ assert.equal(reasons.length, 1);
4013
+ assert.match(reasons[0], /malformed/);
4014
+
4015
+ // A second read served from the warm cache must still say the catalog was incomplete.
4016
+ const again: string[] = [];
4017
+ await catalog.listNamespaces(undefined, { onIncomplete: (reason) => again.push(reason) });
4018
+ assert.equal(again.length, 1, "the cached result keeps reporting the malformed line");
4019
+ } finally {
4020
+ await rm(memoryDir, { recursive: true, force: true });
4021
+ }
4022
+ });
4023
+
4024
+ test("listNamespaces reports an incomplete read for an unreadable catalog (#3293)", async () => {
4025
+ const memoryDir = await mkMemoryDir();
4026
+ try {
4027
+ // A directory where the catalog file should be: open succeeds, reading it fails.
4028
+ await mkdir(path.join(memoryDir, "state", "namespaces.jsonl"), { recursive: true });
4029
+ const catalog = new NamespaceCatalog(makeConfig(memoryDir));
4030
+ const reasons: string[] = [];
4031
+ const records = await catalog.listNamespaces(undefined, { onIncomplete: (reason) => reasons.push(reason) });
4032
+ assert.deepEqual(records, []);
4033
+ assert.equal(reasons.length, 1);
4034
+ assert.match(reasons[0], /unreadable/);
4035
+ } finally {
4036
+ await rm(memoryDir, { recursive: true, force: true });
4037
+ }
4038
+ });
4039
+
4040
+ test("listNamespaces does not report a clean or missing catalog as incomplete (#3293)", async () => {
4041
+ const memoryDir = await mkMemoryDir();
4042
+ try {
4043
+ const catalog = new NamespaceCatalog(makeConfig(memoryDir));
4044
+ const reasons: string[] = [];
4045
+ const onIncomplete = (reason: string) => reasons.push(reason);
4046
+ await catalog.listNamespaces(undefined, { onIncomplete });
4047
+ assert.deepEqual(reasons, [], "a catalog that was never written is normal on a fresh install");
4048
+ await catalog.markWrite("project-origin-abc123", { discoveredBy: "write" });
4049
+ await catalog.listNamespaces(undefined, { onIncomplete });
4050
+ assert.deepEqual(reasons, []);
4051
+ } finally {
4052
+ await rm(memoryDir, { recursive: true, force: true });
4053
+ }
4054
+ });
4055
+
4056
+ test("a catalog with malformed rows stays tolerant for callers that do not ask about completeness (#3293)", async () => {
4057
+ const memoryDir = await mkMemoryDir();
4058
+ try {
4059
+ const catalog = new NamespaceCatalog(makeConfig(memoryDir));
4060
+ await catalog.markWrite("project-origin-abc123", { discoveredBy: "write" });
4061
+ const catalogPath = path.join(memoryDir, "state", "namespaces.jsonl");
4062
+ // Valid JSON that is not a catalog record counts as malformed too.
4063
+ await appendFile(catalogPath, "{\"not\":\"a record\"}\n", "utf8");
4064
+
4065
+ // No options, or options without a callback: nothing to call, nothing may throw.
4066
+ assert.deepEqual((await catalog.listNamespaces()).map((r) => r.namespace), ["project-origin-abc123"]);
4067
+ assert.deepEqual((await catalog.listNamespaces(undefined, {})).map((r) => r.namespace), ["project-origin-abc123"]);
4068
+
4069
+ const reasons: string[] = [];
4070
+ await catalog.listNamespaces(undefined, { onIncomplete: (reason) => reasons.push(reason) });
4071
+ assert.equal(reasons.length, 1, "the non-record row is reported");
4072
+
4073
+ // The warm cache now remembers the problem; a reader without a reporter must still work.
4074
+ assert.equal((await catalog.getNamespaceRecord("project-origin-abc123"))?.namespace, "project-origin-abc123");
4075
+
4076
+ // A cold read of a malformed catalog by a reader without a reporter must work too.
4077
+ const cold = new NamespaceCatalog(makeConfig(memoryDir));
4078
+ assert.equal((await cold.getNamespaceRecord("project-origin-abc123"))?.namespace, "project-origin-abc123");
4079
+ } finally {
4080
+ await rm(memoryDir, { recursive: true, force: true });
4081
+ }
4082
+ });
@@ -87,6 +87,20 @@ export interface NamespaceCatalogFilter {
87
87
  writtenSince?: Date;
88
88
  }
89
89
 
90
+ /** Out-parameter a catalog read fills in when it could not see the whole catalog. */
91
+ interface CatalogReadHealth {
92
+ incomplete?: string;
93
+ }
94
+
95
+ export interface NamespaceCatalogReadOptions {
96
+ /**
97
+ * Called when the read could not see the whole catalog (an unreadable file, or malformed rows
98
+ * that were skipped). The records that were readable are still returned. A catalog that was
99
+ * never written is normal and does not call this.
100
+ */
101
+ onIncomplete?: (reason: string) => void;
102
+ }
103
+
90
104
  export interface NamespaceTouchMetadata {
91
105
  discoveredBy?: NamespaceDiscoverySource;
92
106
  kind?: NamespaceKind;
@@ -412,6 +426,8 @@ interface CatalogFileIdentity {
412
426
  interface CompactedCatalogCache {
413
427
  identity: CatalogFileIdentity;
414
428
  records: Map<string, NamespaceRecord>;
429
+ /** Why the read was incomplete (malformed rows), kept so a cache hit reports it too. */
430
+ incomplete?: string;
415
431
  }
416
432
 
417
433
  export class NamespaceCatalog {
@@ -626,9 +642,9 @@ export class NamespaceCatalog {
626
642
  }
627
643
  return [...byStorageDir.values()];
628
644
  }
629
- private loadSanitizedRecords(): Promise<NamespaceRecord[]> {
645
+ private loadSanitizedRecords(health?: CatalogReadHealth): Promise<NamespaceRecord[]> {
630
646
  return this.queueCritical(async () => {
631
- const records = await this.loadCompacted();
647
+ const records = await this.loadCompacted(false, health);
632
648
  const sanitized = await Promise.all(
633
649
  [...records.values()].map((r) => this.sanitizeRecordForRead(r)),
634
650
  );
@@ -642,9 +658,14 @@ export class NamespaceCatalog {
642
658
  });
643
659
  }
644
660
 
645
- async listNamespaces(filter?: NamespaceCatalogFilter): Promise<NamespaceRecord[]> {
661
+ async listNamespaces(
662
+ filter?: NamespaceCatalogFilter,
663
+ options?: NamespaceCatalogReadOptions,
664
+ ): Promise<NamespaceRecord[]> {
646
665
  if (!this.enabled) return [];
647
- let out = await this.loadSanitizedRecords();
666
+ const health: CatalogReadHealth = {};
667
+ let out = await this.loadSanitizedRecords(health);
668
+ if (health.incomplete !== undefined) options?.onIncomplete?.(health.incomplete);
648
669
  if (filter?.kind) out = out.filter((r) => r.kind === filter.kind);
649
670
  if (filter?.discoveredBy) out = out.filter((r) => r.discoveredBy === filter.discoveredBy);
650
671
  if (filter?.writtenSince) {
@@ -2078,13 +2099,18 @@ export class NamespaceCatalog {
2078
2099
  // ── Persistence ──────────────────────────────────────────────────────────
2079
2100
 
2080
2101
  /** Load the JSONL log and fold it into current state (last-record-wins). */
2081
- private async loadCompacted(forceFresh = false): Promise<Map<string, NamespaceRecord>> {
2102
+ private async loadCompacted(
2103
+ forceFresh = false,
2104
+ health?: CatalogReadHealth,
2105
+ ): Promise<Map<string, NamespaceRecord>> {
2082
2106
  const records = new Map<string, NamespaceRecord>();
2083
2107
  let handle;
2084
2108
  try {
2085
2109
  handle = await open(this.catalogPath, "r");
2086
- } catch {
2110
+ } catch (error) {
2087
2111
  this.compactedCache = undefined;
2112
+ // A catalog that was never written is normal; any other open failure hid the whole catalog.
2113
+ if (health && !isNotFoundError(error)) health.incomplete = `catalog unreadable: ${String(error)}`;
2088
2114
  return records;
2089
2115
  }
2090
2116
 
@@ -2106,6 +2132,7 @@ export class NamespaceCatalog {
2106
2132
  this.compactedCache.identity.ino === identity.ino
2107
2133
  ) {
2108
2134
  await handle.close().catch(() => undefined);
2135
+ if (health && this.compactedCache.incomplete) health.incomplete = this.compactedCache.incomplete;
2109
2136
  return this.cloneCompactedRecords(this.compactedCache.records);
2110
2137
  }
2111
2138
  } catch {
@@ -2113,6 +2140,7 @@ export class NamespaceCatalog {
2113
2140
  // do not cache the result without a trustworthy identity.
2114
2141
  }
2115
2142
 
2143
+ let malformed = 0;
2116
2144
  try {
2117
2145
  this.onCatalogReadForTest?.();
2118
2146
  // Streaming chunked parse (issue #1903): the log can reach 100+ MB in
@@ -2136,11 +2164,16 @@ export class NamespaceCatalog {
2136
2164
  try {
2137
2165
  parsed = JSON.parse(trimmed);
2138
2166
  } catch {
2139
- // Skip corrupt lines (CLAUDE.md rule #18 robustness).
2167
+ // Skip corrupt lines (CLAUDE.md rule #18 robustness), but count them so a caller that
2168
+ // needs the whole catalog can tell the read was incomplete (issue #3293).
2169
+ malformed += 1;
2140
2170
  return;
2141
2171
  }
2142
2172
  const record = coerceRecord(parsed);
2143
- if (!record) return;
2173
+ if (!record) {
2174
+ malformed += 1;
2175
+ return;
2176
+ }
2144
2177
  const prior = records.get(record.namespace);
2145
2178
  records.set(record.namespace, prior ? mergeNewerTouchFields(record, prior) : record);
2146
2179
  };
@@ -2161,14 +2194,17 @@ export class NamespaceCatalog {
2161
2194
  }
2162
2195
  leftover += decoder.end();
2163
2196
  foldLine(leftover);
2164
- } catch {
2197
+ } catch (error) {
2165
2198
  this.compactedCache = undefined;
2199
+ if (health) health.incomplete = `catalog unreadable: ${String(error)}`;
2166
2200
  return new Map();
2167
2201
  } finally {
2168
2202
  await handle.close().catch(() => undefined);
2169
2203
  }
2170
2204
 
2171
- this.compactedCache = identity ? { identity, records } : undefined;
2205
+ const incomplete = malformed > 0 ? `${malformed} malformed catalog line(s) skipped` : undefined;
2206
+ if (health && incomplete) health.incomplete = incomplete;
2207
+ this.compactedCache = identity ? { identity, records, incomplete } : undefined;
2172
2208
  return this.cloneCompactedRecords(records);
2173
2209
  }
2174
2210
 
@@ -43,7 +43,7 @@ import type {
43
43
  NamespaceSearchRouter,
44
44
  NamespaceUpdateResult,
45
45
  } from "../namespaces/search.js";
46
- import type { NamespaceCatalog, NamespaceRecord } from "../namespaces/catalog.js";
46
+ import { NamespaceCatalog, type NamespaceRecord } from "../namespaces/catalog.js";
47
47
  import { namespaceIdentityFromToken, namespaceIdentityToken } from "../namespaces/identity.js";
48
48
  import { isSafeRouteNamespace } from "../routing/engine.js";
49
49
  import { readNamespaceMaintenanceStatuses } from "../maintenance/namespace-planner.js";
@@ -3397,6 +3397,38 @@ test("requestQmdMaintenanceCompletion over namespaces: a failed catalog read lea
3397
3397
  }
3398
3398
  });
3399
3399
 
3400
+ test("requestQmdMaintenanceCompletion over namespaces: a malformed catalog record leaves the request owed (#3293)", async () => {
3401
+ // A real catalog: the malformed row is skipped (the rest is still maintained), but the pass did
3402
+ // not see the whole catalog, so the migration must keep its marker.
3403
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-completion-malformed-"));
3404
+ const config = fixtureConfig({
3405
+ memoryDir,
3406
+ namespacesEnabled: true,
3407
+ defaultNamespace: "default",
3408
+ sharedNamespace: "shared",
3409
+ namespacePolicies: [],
3410
+ maintenanceNamespaceLockStaleMs: 100,
3411
+ qmdAutoEmbedEnabled: false,
3412
+ });
3413
+ await mkdir(path.join(memoryDir, "state"), { recursive: true });
3414
+ await writeFile(path.join(memoryDir, "state", "namespaces.jsonl"), "{this is not json\n", "utf8");
3415
+ const router: NamespaceSearchRouter = Object.assign(Object.create(null), {
3416
+ async updateNamespacesDetailed(ns: string[]): Promise<NamespaceUpdateResult> {
3417
+ return { backendCount: ns.length, eligibleNamespaces: ns };
3418
+ },
3419
+ async embedNamespaces(): Promise<void> {},
3420
+ });
3421
+ const scheduler = buildScheduler({ config, catalog: new NamespaceCatalog(config), router });
3422
+ try {
3423
+ const served = scheduler.requestQmdMaintenanceCompletion();
3424
+ await scheduler.runQmdMaintenance();
3425
+ assert.equal(await served, false);
3426
+ } finally {
3427
+ await scheduler.dispose();
3428
+ await rm(memoryDir, { recursive: true, force: true });
3429
+ }
3430
+ });
3431
+
3400
3432
  test("requestQmdMaintenanceCompletion over namespaces: a permanent skip (branch_disabled) does not block serving", async () => {
3401
3433
  // A cataloged branch namespace is never maintained by default; that is a standing configuration,
3402
3434
  // not an unfinished pass, so the ran namespace must still serve the request.
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/maintenance/namespace-planner.ts"],"sourcesContent":["import { resolveNamespaceCapabilities } from \"../capabilities.js\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport type { Dirent } from \"node:fs\";\nimport { lstat, mkdir, open, readFile, readdir, rename, rm, rmdir, stat, utimes, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport type { NamespaceCatalog, NamespaceKind, NamespaceRecord } from \"../namespaces/catalog.js\";\nimport { hasMemoryData } from \"../namespaces/catalog.js\";\nimport { namespaceIdentityToken } from \"../namespaces/identity.js\";\nimport { resolveNamespaceStorageRoot } from \"../namespaces/storage.js\";\nimport { displayErrorDetail } from \"../runtime/better-sqlite.js\";\nimport type { PluginConfig } from \"../types.js\";\nimport { getConfiguredNamespaces } from \"../scopes/scope-plan.js\";\nimport { resolveConversationContextCapabilities } from \"../capabilities.js\";\n\nexport type NamespaceMaintenanceJobName = string;\n\nexport type NamespaceMaintenanceSkipReason =\n | \"fanout_disabled\"\n | \"empty_root\"\n | \"branch_disabled\"\n | \"project_disabled\"\n | \"team_project_disabled\"\n | \"catalog_read_failed\"\n | \"unsafe_or_stale_root\"\n | \"budget_exhausted\"\n | \"lock_held\"\n | \"batch_lock_incomplete\"\n | \"throttled\"\n | \"job_failed\";\n\nexport interface NamespaceMaintenancePlannerOptions {\n jobName: NamespaceMaintenanceJobName;\n catalog?: NamespaceCatalog;\n now?: Date;\n budgetMode?: \"cycle\" | \"unbounded\";\n}\n\nexport interface NamespaceMaintenanceCandidate {\n namespace: string;\n kind: NamespaceKind;\n storageDir?: string;\n source: \"configured\" | \"catalog\";\n lastWriteAt?: string;\n lastMaintenanceAt?: string;\n}\n\nexport interface NamespaceMaintenancePlan {\n jobName: NamespaceMaintenanceJobName;\n generatedAt: string;\n namespaces: NamespaceMaintenanceCandidate[];\n skipped: NamespaceMaintenanceSkippedNamespace[];\n budget: {\n maxNamespacesPerCycle: number;\n selected: number;\n };\n}\n\nexport interface NamespaceMaintenanceSkippedNamespace {\n namespace: string;\n kind?: NamespaceKind;\n reason: NamespaceMaintenanceSkipReason;\n detail?: string;\n}\n\nexport interface NamespaceMaintenanceRunStatus {\n namespace: string;\n jobName: NamespaceMaintenanceJobName;\n state: \"ran\" | \"skipped\" | \"failed\";\n reason?: NamespaceMaintenanceSkipReason;\n startedAt: string;\n completedAt: string;\n itemCount?: number;\n error?: string;\n}\n\nexport interface NamespaceMaintenanceSummary {\n jobName: NamespaceMaintenanceJobName;\n generatedAt: string;\n ran: number;\n skipped: number;\n failed: number;\n statuses: NamespaceMaintenanceRunStatus[];\n}\n\nexport interface NamespaceMaintenanceBatchRunResult {\n itemCount?: number;\n itemCounts?: Record<string, number> | Map<string, number>;\n}\n\nexport interface NamespaceMaintenanceBatchRunOptions {\n requireAllLocks?: boolean;\n skipReasonForError?: (error: unknown) => NamespaceMaintenanceSkipReason | null | undefined;\n}\n\ninterface LockHandle {\n path: string;\n touch(): Promise<void>;\n release(): Promise<void>;\n}\n\nconst DEFAULT_MAX_NAMESPACES_PER_CYCLE = 20;\nconst DEFAULT_LOCK_STALE_MS = 10 * 60_000;\nconst LOCK_BASE = \"maintenance-locks\";\nconst STATUS_BASE = \"namespace-maintenance-status\";\nconst namespaceMaintenanceFs = { open, rm };\n\nexport function __setNamespaceMaintenanceFsForTest(overrides: Partial<typeof namespaceMaintenanceFs>): () => void {\n const previous = { ...namespaceMaintenanceFs };\n Object.assign(namespaceMaintenanceFs, overrides);\n return () => {\n Object.assign(namespaceMaintenanceFs, previous);\n };\n}\n\n\n\nfunction inferConfiguredKind(config: PluginConfig, namespace: string): NamespaceKind {\n if (namespace === config.defaultNamespace.trim()) return \"default\";\n if (namespace === config.sharedNamespace.trim()) return \"shared\";\n return \"explicit\";\n}\n\nfunction maxNamespacesPerCycle(config: PluginConfig): number {\n return Math.max(\n 1,\n Math.floor(\n typeof config.maintenanceMaxNamespacesPerCycle === \"number\" &&\n Number.isFinite(config.maintenanceMaxNamespacesPerCycle)\n ? config.maintenanceMaxNamespacesPerCycle\n : DEFAULT_MAX_NAMESPACES_PER_CYCLE\n )\n );\n}\n\nfunction namespaceKindAllowed(config: PluginConfig, kind: NamespaceKind): boolean {\n switch (kind) {\n case \"branch\":\n return config.maintenanceIncludeBranchNamespaces === true;\n case \"project\":\n return config.maintenanceIncludeProjectNamespaces !== false;\n case \"team-project\":\n return config.maintenanceIncludeTeamProjectNamespaces !== false;\n default:\n return true;\n }\n}\n\nfunction disabledReasonForKind(kind: NamespaceKind): NamespaceMaintenanceSkipReason {\n if (kind === \"branch\") return \"branch_disabled\";\n if (kind === \"project\") return \"project_disabled\";\n if (kind === \"team-project\") return \"team_project_disabled\";\n return \"fanout_disabled\";\n}\n\nasync function catalogRootIsLive(config: PluginConfig, record: NamespaceRecord): Promise<boolean> {\n if (typeof record.storageDir !== \"string\" || record.storageDir.length === 0) {\n return false;\n }\n try {\n const liveRoot = await resolveNamespaceStorageRoot(config, record.namespace);\n if (path.resolve(liveRoot) !== path.resolve(record.storageDir)) return false;\n return hasMemoryData(liveRoot);\n } catch {\n return false;\n }\n}\n\nfunction candidateSortKey(candidate: NamespaceMaintenanceCandidate): string {\n const write = candidate.lastWriteAt ?? \"\";\n return `${write}\\u0000${candidate.namespace}`;\n}\n\nfunction candidatePriority(candidate: NamespaceMaintenanceCandidate): number {\n if (candidate.kind === \"default\") return 0;\n if (candidate.kind === \"shared\") return 1;\n if (candidate.source === \"configured\") return 2;\n if (candidate.kind === \"team-project\") return 3;\n if (candidate.kind === \"project\") return 4;\n if (candidate.kind === \"branch\") return 8;\n return 7;\n}\n\nfunction sortCandidates(a: NamespaceMaintenanceCandidate, b: NamespaceMaintenanceCandidate): number {\n const priority = candidatePriority(a) - candidatePriority(b);\n if (priority !== 0) return priority;\n const am = Date.parse(a.lastMaintenanceAt ?? \"\");\n const bm = Date.parse(b.lastMaintenanceAt ?? \"\");\n const aMaintained = Number.isFinite(am);\n const bMaintained = Number.isFinite(bm);\n if (aMaintained && bMaintained && am !== bm) return am - bm;\n if (aMaintained !== bMaintained) return aMaintained ? 1 : -1;\n const aw = Date.parse(a.lastWriteAt ?? \"\");\n const bw = Date.parse(b.lastWriteAt ?? \"\");\n const aValid = Number.isFinite(aw);\n const bValid = Number.isFinite(bw);\n if (aValid && bValid && aw !== bw) return bw - aw;\n if (aValid !== bValid) return aValid ? -1 : 1;\n const byKey = candidateSortKey(a).localeCompare(candidateSortKey(b));\n if (byKey !== 0) return byKey;\n return a.namespace.localeCompare(b.namespace);\n}\n\nexport async function planNamespaceMaintenance(\n config: PluginConfig,\n options: NamespaceMaintenancePlannerOptions\n): Promise<NamespaceMaintenancePlan> {\n const generatedAt = (options.now ?? new Date()).toISOString();\n // When namespaces are disabled, storageFor() collapses every namespace name\n // to config.memoryDir. Seeding all configured namespaces (default + shared +\n // policies) would make a mutating maintenance job process the SAME corpus\n // once per configured name. The #1500 contract is \"namespaces disabled:\n // maintain the current default storage only,\" so collapse to the default.\n const configured = resolveNamespaceCapabilities(config).namespaces\n ? getConfiguredNamespaces(config)\n : [config.defaultNamespace.trim()].filter(Boolean);\n const byNamespace = new Map<string, NamespaceMaintenanceCandidate>();\n const skipped: NamespaceMaintenanceSkippedNamespace[] = [];\n\n for (const namespace of configured) {\n const kind = inferConfiguredKind(config, namespace);\n byNamespace.set(namespace, {\n namespace,\n kind,\n source: \"configured\",\n });\n }\n\n if (resolveNamespaceCapabilities(config).namespaces && resolveConversationContextCapabilities(config).maintenanceNamespaceFanout !== false) {\n const configuredSet = new Set(configured);\n try {\n const records = options.catalog?.enabled ? await options.catalog.listNamespaces() : [];\n for (const record of records) {\n const namespace = record.namespace.trim();\n if (!namespace) continue;\n const isConfigured = configuredSet.has(namespace);\n const kind = isConfigured ? inferConfiguredKind(config, namespace) : record.kind;\n if (!namespaceKindAllowed(config, kind)) {\n skipped.push({\n namespace,\n kind,\n reason: disabledReasonForKind(kind),\n });\n continue;\n }\n if (!isConfigured && !(await catalogRootIsLive(config, record))) {\n skipped.push({\n namespace,\n kind,\n reason: \"unsafe_or_stale_root\",\n });\n continue;\n }\n byNamespace.set(namespace, {\n namespace,\n kind,\n storageDir: record.storageDir,\n source: isConfigured ? \"configured\" : \"catalog\",\n lastWriteAt: record.lastWriteAt,\n lastMaintenanceAt: record.lastMaintenanceAt?.[options.jobName],\n });\n }\n } catch (error) {\n skipped.push({\n namespace: \"*\",\n reason: \"catalog_read_failed\",\n detail: error instanceof Error ? error.message : String(error),\n });\n }\n } else if (resolveNamespaceCapabilities(config).namespaces) {\n skipped.push({\n namespace: \"*\",\n reason: \"fanout_disabled\",\n });\n }\n\n if (options.budgetMode !== \"unbounded\") {\n const latestStatusAtByNamespace = await readLatestStatusAtByNamespace(config, options.jobName);\n for (const candidate of byNamespace.values()) {\n if (!candidate.lastMaintenanceAt) {\n candidate.lastMaintenanceAt = latestStatusAtByNamespace.get(candidate.namespace);\n }\n }\n }\n\n const candidates = [...byNamespace.values()]\n .filter((candidate) => namespaceKindAllowed(config, candidate.kind))\n .sort(sortCandidates);\n\n const max = maxNamespacesPerCycle(config);\n const applyCycleBudget = options.budgetMode !== \"unbounded\";\n const selected = applyCycleBudget ? candidates.slice(0, max) : candidates;\n if (applyCycleBudget) {\n for (const candidate of candidates.slice(max)) {\n skipped.push({\n namespace: candidate.namespace,\n kind: candidate.kind,\n reason: \"budget_exhausted\",\n });\n }\n }\n\n return {\n jobName: options.jobName,\n generatedAt,\n namespaces: selected,\n skipped,\n budget: {\n maxNamespacesPerCycle: max,\n selected: selected.length,\n },\n };\n}\n\nfunction stablePathSegment(value: string): string {\n const sanitized = value.replace(/[^a-zA-Z0-9._-]/g, \"_\").slice(0, 128) || \"unnamed\";\n if (sanitized.length <= 128 && sanitized === value) return sanitized;\n return `${sanitized.slice(0, 80)}-${createHash(\"sha256\").update(value).digest(\"hex\").slice(0, 16)}`;\n}\n\nfunction namespacePathSegment(namespace: string): string {\n const token = namespaceIdentityToken(namespace);\n if (token.length <= 160) return token;\n return `ns-${createHash(\"sha256\").update(namespace).digest(\"hex\")}`;\n}\n\nfunction lockPath(config: PluginConfig, jobName: string, namespace: string): string {\n return path.join(\n config.memoryDir,\n \"state\",\n LOCK_BASE,\n stablePathSegment(jobName),\n `${namespacePathSegment(namespace)}.lock`\n );\n}\n\nfunction namespaceMaintenanceLockStaleMs(config: PluginConfig): number {\n if (\n typeof config.maintenanceNamespaceLockStaleMs === \"number\" &&\n Number.isFinite(config.maintenanceNamespaceLockStaleMs) &&\n config.maintenanceNamespaceLockStaleMs > 0\n ) {\n return Math.floor(config.maintenanceNamespaceLockStaleMs);\n }\n return DEFAULT_LOCK_STALE_MS;\n}\n\nfunction namespaceMaintenanceLockHeartbeatMs(config: PluginConfig): number {\n const staleMs = namespaceMaintenanceLockStaleMs(config);\n return Math.max(1, Math.min(30_000, Math.floor(staleMs / 3) || 1));\n}\n\nfunction errorCode(error: unknown): string | undefined {\n return typeof error === \"object\" && error !== null && \"code\" in error\n ? (error as { code?: string }).code\n : undefined;\n}\n\nasync function withNamespaceMaintenanceLockHeartbeat<T>(\n config: PluginConfig,\n locks: LockHandle | LockHandle[],\n task: () => Promise<T>,\n): Promise<T> {\n const activeLocks = Array.isArray(locks) ? locks : [locks];\n const interval = setInterval(() => {\n for (const lock of activeLocks) {\n void lock.touch().catch(() => undefined);\n }\n }, namespaceMaintenanceLockHeartbeatMs(config));\n interval.unref?.();\n try {\n return await task();\n } finally {\n clearInterval(interval);\n }\n}\n\nasync function removeStaleLockDirectory(filePath: string): Promise<void> {\n let entries: Dirent[];\n try {\n entries = await readdir(filePath, { withFileTypes: true });\n } catch (error) {\n if (errorCode(error) === \"ENOENT\") return;\n throw error;\n }\n for (const entry of entries) {\n if (!entry.isFile()) continue;\n await namespaceMaintenanceFs.rm(path.join(filePath, entry.name), { force: true });\n }\n try {\n await rmdir(filePath);\n } catch (error) {\n if (errorCode(error) === \"ENOENT\") return;\n throw error;\n }\n}\n\nasync function tryAcquireNamespaceMaintenanceLock(\n config: PluginConfig,\n jobName: string,\n namespace: string\n): Promise<LockHandle | null> {\n const filePath = lockPath(config, jobName, namespace);\n await mkdir(path.dirname(filePath), { recursive: true });\n try {\n const lockId = randomUUID();\n await mkdir(filePath);\n const ownerPath = path.join(filePath, `${lockId}.json`);\n let handle: Awaited<ReturnType<typeof open>> | undefined;\n try {\n handle = await namespaceMaintenanceFs.open(ownerPath, \"wx\");\n await handle.writeFile(\n `${JSON.stringify({\n lockId,\n pid: process.pid,\n jobName,\n namespace,\n acquiredAt: new Date().toISOString(),\n })}\\n`,\n \"utf8\"\n );\n await handle.close();\n } catch (setupError) {\n await handle?.close().catch(() => undefined);\n await namespaceMaintenanceFs.rm(ownerPath, { force: true }).catch(() => undefined);\n await rmdir(filePath).catch(() => undefined);\n throw setupError;\n }\n return {\n path: filePath,\n async touch() {\n try {\n const parsed = JSON.parse(await readFile(ownerPath, \"utf8\")) as { lockId?: unknown };\n if (parsed.lockId === lockId) {\n const now = new Date();\n await utimes(ownerPath, now, now);\n await utimes(filePath, now, now);\n }\n } catch {}\n },\n async release() {\n try {\n const parsed = JSON.parse(await readFile(ownerPath, \"utf8\")) as { lockId?: unknown };\n if (parsed.lockId === lockId) {\n await namespaceMaintenanceFs.rm(ownerPath, { force: true });\n await rmdir(filePath).catch(() => undefined);\n }\n } catch {}\n },\n };\n } catch (error) {\n if (errorCode(error) === \"EEXIST\") {\n const staleMs =\n namespaceMaintenanceLockStaleMs(config);\n try {\n const s = await lstat(filePath);\n if (s.isSymbolicLink()) {\n return null;\n }\n if ((s.isFile() || s.isDirectory()) && Date.now() - s.mtimeMs > staleMs) {\n try {\n if (s.isDirectory()) {\n await removeStaleLockDirectory(filePath);\n } else {\n await namespaceMaintenanceFs.rm(filePath, { force: true });\n }\n } catch (removeError) {\n if (errorCode(removeError) === \"ENOENT\") {\n return tryAcquireNamespaceMaintenanceLock(config, jobName, namespace);\n }\n if (errorCode(removeError) === \"ENOTEMPTY\") {\n return null;\n }\n throw removeError;\n }\n return tryAcquireNamespaceMaintenanceLock(config, jobName, namespace);\n }\n } catch (statError) {\n if (errorCode(statError) === \"ENOENT\") {\n return tryAcquireNamespaceMaintenanceLock(config, jobName, namespace);\n }\n throw statError;\n }\n return null;\n }\n throw error;\n }\n}\n\nfunction statusBasePath(config: PluginConfig): string {\n return path.join(config.memoryDir, \"state\", STATUS_BASE);\n}\n\nfunction statusPath(config: PluginConfig, jobName: string, namespace: string): string {\n return path.join(statusBasePath(config), stablePathSegment(jobName), `${namespacePathSegment(namespace)}.json`);\n}\n\nfunction lastRanStatusPath(config: PluginConfig, jobName: string, namespace: string): string {\n return path.join(statusBasePath(config), stablePathSegment(jobName), `${namespacePathSegment(namespace)}.last-ran.json`);\n}\n\nfunction parseStatus(value: unknown): NamespaceMaintenanceRunStatus | null {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return null;\n const v = value as Partial<NamespaceMaintenanceRunStatus>;\n if (\n typeof v.namespace === \"string\" &&\n typeof v.jobName === \"string\" &&\n (v.state === \"ran\" || v.state === \"skipped\" || v.state === \"failed\") &&\n typeof v.startedAt === \"string\" &&\n typeof v.completedAt === \"string\"\n ) {\n return v as NamespaceMaintenanceRunStatus;\n }\n return null;\n}\n\nasync function readLatestStatusAtByNamespace(config: PluginConfig, jobName: string): Promise<Map<string, string>> {\n const latest = new Map<string, string>();\n const latestMs = new Map<string, number>();\n for (const status of [...(await readStatusFiles(config)), ...(await readLastRanStatusFiles(config))]) {\n if (status.state !== \"ran\") continue;\n if (status.jobName !== jobName) continue;\n const completedAtMs = Date.parse(status.completedAt);\n if (!Number.isFinite(completedAtMs)) continue;\n const previousMs = latestMs.get(status.namespace);\n if (previousMs !== undefined && previousMs >= completedAtMs) continue;\n latestMs.set(status.namespace, completedAtMs);\n latest.set(status.namespace, status.completedAt);\n }\n return latest;\n}\n\nasync function readStatusFile(filePath: string): Promise<NamespaceMaintenanceRunStatus | null> {\n try {\n const parsed = JSON.parse(await readFile(filePath, \"utf8\")) as unknown;\n return parseStatus(parsed);\n } catch {\n return null;\n }\n}\n\nasync function readStatusFiles(config: PluginConfig): Promise<NamespaceMaintenanceRunStatus[]> {\n if (typeof config.memoryDir !== \"string\" || config.memoryDir.length === 0) {\n return [];\n }\n const root = statusBasePath(config);\n const statuses: NamespaceMaintenanceRunStatus[] = [];\n let jobDirs: Dirent[];\n try {\n jobDirs = await readdir(root, { withFileTypes: true });\n } catch {\n return statuses;\n }\n for (const jobDir of jobDirs) {\n if (!jobDir.isDirectory()) continue;\n let files: Dirent[];\n try {\n files = await readdir(path.join(root, jobDir.name), { withFileTypes: true });\n } catch {\n continue;\n }\n for (const file of files) {\n if (!file.isFile() || !file.name.endsWith(\".json\")) continue;\n if (file.name.endsWith(\".last-ran.json\")) continue;\n const status = await readStatusFile(path.join(root, jobDir.name, file.name));\n if (status) statuses.push(status);\n }\n }\n return statuses;\n}\n\nasync function readLastRanStatusFiles(config: PluginConfig): Promise<NamespaceMaintenanceRunStatus[]> {\n if (typeof config.memoryDir !== \"string\" || config.memoryDir.length === 0) {\n return [];\n }\n const root = statusBasePath(config);\n const statuses: NamespaceMaintenanceRunStatus[] = [];\n let jobDirs: Dirent[];\n try {\n jobDirs = await readdir(root, { withFileTypes: true });\n } catch {\n return statuses;\n }\n for (const jobDir of jobDirs) {\n if (!jobDir.isDirectory()) continue;\n let files: Dirent[];\n try {\n files = await readdir(path.join(root, jobDir.name), { withFileTypes: true });\n } catch {\n continue;\n }\n for (const file of files) {\n if (!file.isFile() || !file.name.endsWith(\".last-ran.json\")) continue;\n const status = await readStatusFile(path.join(root, jobDir.name, file.name));\n if (status) statuses.push(status);\n }\n }\n return statuses;\n}\n\nasync function writeStatusPayload(target: string, status: NamespaceMaintenanceRunStatus): Promise<void> {\n const dir = path.dirname(target);\n await mkdir(dir, { recursive: true });\n const temp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;\n const payload = {\n version: 1,\n ...status,\n };\n await writeFile(temp, `${JSON.stringify(payload, null, 2)}\\n`, \"utf8\");\n await rename(temp, target);\n}\n\nasync function writeStatusFile(config: PluginConfig, status: NamespaceMaintenanceRunStatus): Promise<void> {\n await writeStatusPayload(statusPath(config, status.jobName, status.namespace), status);\n if (status.state === \"ran\") {\n await writeStatusPayload(lastRanStatusPath(config, status.jobName, status.namespace), status);\n }\n}\n\nasync function recordNamespaceMaintenanceStatusSafely(\n config: PluginConfig,\n status: NamespaceMaintenanceRunStatus\n): Promise<void> {\n try {\n await writeStatusFile(config, status);\n } catch {\n // Observability must not fail the maintenance operation.\n }\n}\n\nfunction maintenanceErrorDetail(error: unknown): string {\n return displayErrorDetail(error) || \"Error\";\n}\n\nexport async function readNamespaceMaintenanceStatuses(config: PluginConfig): Promise<NamespaceMaintenanceRunStatus[]> {\n return (await readStatusFiles(config)).sort((a, b) => {\n const byJob = a.jobName.localeCompare(b.jobName);\n if (byJob !== 0) return byJob;\n return a.namespace.localeCompare(b.namespace);\n });\n}\n\n/**\n * Read the last SUCCESSFUL run status per job+namespace (the\n * `<namespace>.last-ran.json` files written only when state === \"ran\").\n *\n * The latest status file (`<namespace>.json`) is overwritten on every run,\n * so after a successful run followed by a skip (budget/lock/cadence) the\n * latest file shows \"skipped\" and the prior success is invisible. Merging\n * these last-ran records into the health summary lets `lastRunAt` and run\n * history reflect the most recent successful maintenance (review #1622).\n */\nexport async function readNamespaceMaintenanceLastRanStatuses(\n config: PluginConfig,\n): Promise<NamespaceMaintenanceRunStatus[]> {\n return (await readLastRanStatusFiles(config)).sort((a, b) => {\n const byJob = a.jobName.localeCompare(b.jobName);\n if (byJob !== 0) return byJob;\n return a.namespace.localeCompare(b.namespace);\n });\n}\n\nexport type NamespaceMaintenancePlanRunnerResult = {\n itemCount?: number;\n /**\n * When `true`, the runner performed NO work for this namespace (e.g. the\n * job's own cadence gate throttled it). The planner records the namespace\n * as `state: \"skipped\"` with the given reason and does NOT touch the\n * catalog's `lastMaintenanceAt`, so a throttled namespace is not falsely\n * reported as maintained. Without this signal a runner that resolves\n * without throwing is always recorded as `state: \"ran\"`.\n */\n skipped?: boolean;\n skipReason?: string;\n} | undefined;\n\nexport async function runNamespaceMaintenancePlan(\n config: PluginConfig,\n plan: NamespaceMaintenancePlan,\n runner: (candidate: NamespaceMaintenanceCandidate) => Promise<NamespaceMaintenancePlanRunnerResult>,\n catalog?: NamespaceCatalog\n): Promise<NamespaceMaintenanceSummary> {\n const statuses: NamespaceMaintenanceRunStatus[] = [];\n\n for (const skipped of plan.skipped) {\n if (skipped.namespace === \"*\") continue;\n const now = new Date().toISOString();\n const status: NamespaceMaintenanceRunStatus = {\n namespace: skipped.namespace,\n jobName: plan.jobName,\n state: \"skipped\",\n reason: skipped.reason,\n startedAt: now,\n completedAt: now,\n };\n statuses.push(status);\n await recordNamespaceMaintenanceStatusSafely(config, status);\n }\n\n for (const candidate of plan.namespaces) {\n const startedAt = new Date().toISOString();\n const lock = await tryAcquireNamespaceMaintenanceLock(config, plan.jobName, candidate.namespace);\n if (!lock) {\n const completedAt = new Date().toISOString();\n const status: NamespaceMaintenanceRunStatus = {\n namespace: candidate.namespace,\n jobName: plan.jobName,\n state: \"skipped\",\n reason: \"lock_held\",\n startedAt,\n completedAt,\n };\n statuses.push(status);\n await recordNamespaceMaintenanceStatusSafely(config, status);\n continue;\n }\n try {\n const result = await withNamespaceMaintenanceLockHeartbeat(config, lock, () => runner(candidate));\n const completedAt = new Date().toISOString();\n if (result?.skipped) {\n // The runner performed no work (e.g. the job's own cadence gate\n // throttled this namespace). Record skipped WITHOUT touching the\n // catalog's lastMaintenanceAt so a throttled namespace is not\n // falsely reported as maintained.\n const status: NamespaceMaintenanceRunStatus = {\n namespace: candidate.namespace,\n jobName: plan.jobName,\n state: \"skipped\",\n reason: (result.skipReason ?? \"throttled\") as NamespaceMaintenanceSkipReason,\n startedAt,\n completedAt,\n };\n statuses.push(status);\n await recordNamespaceMaintenanceStatusSafely(config, status);\n } else {\n const status: NamespaceMaintenanceRunStatus = {\n namespace: candidate.namespace,\n jobName: plan.jobName,\n state: \"ran\",\n startedAt,\n completedAt,\n itemCount: result?.itemCount,\n };\n statuses.push(status);\n await recordNamespaceMaintenanceStatusSafely(config, status);\n try {\n await catalog?.markMaintenance(candidate.namespace, plan.jobName, new Date(completedAt));\n } catch {\n // Catalog maintenance touches are best-effort status metadata.\n }\n }\n } catch (error) {\n const completedAt = new Date().toISOString();\n const status: NamespaceMaintenanceRunStatus = {\n namespace: candidate.namespace,\n jobName: plan.jobName,\n state: \"failed\",\n reason: \"job_failed\",\n startedAt,\n completedAt,\n error: maintenanceErrorDetail(error),\n };\n statuses.push(status);\n await recordNamespaceMaintenanceStatusSafely(config, status);\n } finally {\n await lock.release().catch(() => undefined);\n }\n }\n\n return {\n jobName: plan.jobName,\n generatedAt: new Date().toISOString(),\n ran: statuses.filter((s) => s.state === \"ran\").length,\n skipped: statuses.filter((s) => s.state === \"skipped\").length,\n failed: statuses.filter((s) => s.state === \"failed\").length,\n statuses,\n };\n}\n\nexport async function runNamespaceMaintenanceBatchPlan(\n config: PluginConfig,\n plan: NamespaceMaintenancePlan,\n runner: (candidates: NamespaceMaintenanceCandidate[]) => Promise<NamespaceMaintenanceBatchRunResult | undefined>,\n catalog?: NamespaceCatalog,\n options: NamespaceMaintenanceBatchRunOptions = {}\n): Promise<NamespaceMaintenanceSummary> {\n const statuses: NamespaceMaintenanceRunStatus[] = [];\n\n for (const skipped of plan.skipped) {\n if (skipped.namespace === \"*\") continue;\n const now = new Date().toISOString();\n const status: NamespaceMaintenanceRunStatus = {\n namespace: skipped.namespace,\n jobName: plan.jobName,\n state: \"skipped\",\n reason: skipped.reason,\n startedAt: now,\n completedAt: now,\n };\n statuses.push(status);\n await recordNamespaceMaintenanceStatusSafely(config, status);\n }\n\n const acquired: Array<{\n candidate: NamespaceMaintenanceCandidate;\n lock: LockHandle;\n startedAt: string;\n }> = [];\n\n try {\n for (const candidate of plan.namespaces) {\n const startedAt = new Date().toISOString();\n const lock = await tryAcquireNamespaceMaintenanceLock(config, plan.jobName, candidate.namespace);\n if (!lock) {\n const completedAt = new Date().toISOString();\n const status: NamespaceMaintenanceRunStatus = {\n namespace: candidate.namespace,\n jobName: plan.jobName,\n state: \"skipped\",\n reason: \"lock_held\",\n startedAt,\n completedAt,\n };\n statuses.push(status);\n await recordNamespaceMaintenanceStatusSafely(config, status);\n continue;\n }\n acquired.push({ candidate, lock, startedAt });\n }\n } catch (error) {\n await Promise.all(acquired.map(({ lock }) => lock.release().catch(() => undefined)));\n throw error;\n }\n\n if (options.requireAllLocks && acquired.length > 0 && acquired.length < plan.namespaces.length) {\n for (const { candidate, startedAt } of acquired) {\n const completedAt = new Date().toISOString();\n const status: NamespaceMaintenanceRunStatus = {\n namespace: candidate.namespace,\n jobName: plan.jobName,\n state: \"skipped\",\n reason: \"batch_lock_incomplete\",\n startedAt,\n completedAt,\n };\n statuses.push(status);\n await recordNamespaceMaintenanceStatusSafely(config, status);\n }\n await Promise.all(acquired.map(({ lock }) => lock.release().catch(() => undefined)));\n return {\n jobName: plan.jobName,\n generatedAt: new Date().toISOString(),\n ran: statuses.filter((s) => s.state === \"ran\").length,\n skipped: statuses.filter((s) => s.state === \"skipped\").length,\n failed: statuses.filter((s) => s.state === \"failed\").length,\n statuses,\n };\n }\n\n if (acquired.length === 0) {\n return {\n jobName: plan.jobName,\n generatedAt: new Date().toISOString(),\n ran: statuses.filter((s) => s.state === \"ran\").length,\n skipped: statuses.filter((s) => s.state === \"skipped\").length,\n failed: statuses.filter((s) => s.state === \"failed\").length,\n statuses,\n };\n }\n\n try {\n const result = await withNamespaceMaintenanceLockHeartbeat(\n config,\n acquired.map(({ lock }) => lock),\n () => runner(acquired.map(({ candidate }) => candidate)),\n );\n for (const { candidate, startedAt } of acquired) {\n const completedAt = new Date().toISOString();\n const status: NamespaceMaintenanceRunStatus = {\n namespace: candidate.namespace,\n jobName: plan.jobName,\n state: \"ran\",\n startedAt,\n completedAt,\n itemCount: itemCountForNamespace(result, candidate.namespace),\n };\n statuses.push(status);\n await recordNamespaceMaintenanceStatusSafely(config, status);\n try {\n await catalog?.markMaintenance(candidate.namespace, plan.jobName, new Date(completedAt));\n } catch {\n // Catalog maintenance touches are best-effort status metadata.\n }\n }\n } catch (error) {\n const skipReason = options.skipReasonForError?.(error);\n for (const { candidate, startedAt } of acquired) {\n const completedAt = new Date().toISOString();\n const status: NamespaceMaintenanceRunStatus = skipReason\n ? {\n namespace: candidate.namespace,\n jobName: plan.jobName,\n state: \"skipped\",\n reason: skipReason,\n startedAt,\n completedAt,\n }\n : {\n namespace: candidate.namespace,\n jobName: plan.jobName,\n state: \"failed\",\n reason: \"job_failed\",\n startedAt,\n completedAt,\n error: maintenanceErrorDetail(error),\n };\n statuses.push(status);\n await recordNamespaceMaintenanceStatusSafely(config, status);\n }\n } finally {\n await Promise.all(acquired.map(({ lock }) => lock.release().catch(() => undefined)));\n }\n\n return {\n jobName: plan.jobName,\n generatedAt: new Date().toISOString(),\n ran: statuses.filter((s) => s.state === \"ran\").length,\n skipped: statuses.filter((s) => s.state === \"skipped\").length,\n failed: statuses.filter((s) => s.state === \"failed\").length,\n statuses,\n };\n}\n\nfunction itemCountForNamespace(\n result: NamespaceMaintenanceBatchRunResult | undefined,\n namespace: string,\n): number | undefined {\n const itemCounts = result?.itemCounts;\n if (itemCounts instanceof Map) return itemCounts.get(namespace);\n if (itemCounts && Object.prototype.hasOwnProperty.call(itemCounts, namespace)) {\n return itemCounts[namespace];\n }\n return result?.itemCount;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AACA,SAAS,YAAY,kBAAkB;AAEvC,SAAS,OAAO,OAAO,MAAM,UAAU,SAAS,QAAQ,IAAI,OAAa,QAAQ,iBAAiB;AAClG,OAAO,UAAU;AAiGjB,IAAM,mCAAmC;AACzC,IAAM,wBAAwB,KAAK;AACnC,IAAM,YAAY;AAClB,IAAM,cAAc;AACpB,IAAM,yBAAyB,EAAE,MAAM,GAAG;AAY1C,SAAS,oBAAoB,QAAsB,WAAkC;AACnF,MAAI,cAAc,OAAO,iBAAiB,KAAK,EAAG,QAAO;AACzD,MAAI,cAAc,OAAO,gBAAgB,KAAK,EAAG,QAAO;AACxD,SAAO;AACT;AAEA,SAAS,sBAAsB,QAA8B;AAC3D,SAAO,KAAK;AAAA,IACV;AAAA,IACA,KAAK;AAAA,MACH,OAAO,OAAO,qCAAqC,YACjD,OAAO,SAAS,OAAO,gCAAgC,IACrD,OAAO,mCACP;AAAA,IACN;AAAA,EACF;AACF;AAEA,SAAS,qBAAqB,QAAsB,MAA8B;AAChF,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO,OAAO,uCAAuC;AAAA,IACvD,KAAK;AACH,aAAO,OAAO,wCAAwC;AAAA,IACxD,KAAK;AACH,aAAO,OAAO,4CAA4C;AAAA,IAC5D;AACE,aAAO;AAAA,EACX;AACF;AAEA,SAAS,sBAAsB,MAAqD;AAClF,MAAI,SAAS,SAAU,QAAO;AAC9B,MAAI,SAAS,UAAW,QAAO;AAC/B,MAAI,SAAS,eAAgB,QAAO;AACpC,SAAO;AACT;AAEA,eAAe,kBAAkB,QAAsB,QAA2C;AAChG,MAAI,OAAO,OAAO,eAAe,YAAY,OAAO,WAAW,WAAW,GAAG;AAC3E,WAAO;AAAA,EACT;AACA,MAAI;AACF,UAAM,WAAW,MAAM,4BAA4B,QAAQ,OAAO,SAAS;AAC3E,QAAI,KAAK,QAAQ,QAAQ,MAAM,KAAK,QAAQ,OAAO,UAAU,EAAG,QAAO;AACvE,WAAO,cAAc,QAAQ;AAAA,EAC/B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,iBAAiB,WAAkD;AAC1E,QAAM,QAAQ,UAAU,eAAe;AACvC,SAAO,GAAG,KAAK,KAAS,UAAU,SAAS;AAC7C;AAEA,SAAS,kBAAkB,WAAkD;AAC3E,MAAI,UAAU,SAAS,UAAW,QAAO;AACzC,MAAI,UAAU,SAAS,SAAU,QAAO;AACxC,MAAI,UAAU,WAAW,aAAc,QAAO;AAC9C,MAAI,UAAU,SAAS,eAAgB,QAAO;AAC9C,MAAI,UAAU,SAAS,UAAW,QAAO;AACzC,MAAI,UAAU,SAAS,SAAU,QAAO;AACxC,SAAO;AACT;AAEA,SAAS,eAAe,GAAkC,GAA0C;AAClG,QAAM,WAAW,kBAAkB,CAAC,IAAI,kBAAkB,CAAC;AAC3D,MAAI,aAAa,EAAG,QAAO;AAC3B,QAAM,KAAK,KAAK,MAAM,EAAE,qBAAqB,EAAE;AAC/C,QAAM,KAAK,KAAK,MAAM,EAAE,qBAAqB,EAAE;AAC/C,QAAM,cAAc,OAAO,SAAS,EAAE;AACtC,QAAM,cAAc,OAAO,SAAS,EAAE;AACtC,MAAI,eAAe,eAAe,OAAO,GAAI,QAAO,KAAK;AACzD,MAAI,gBAAgB,YAAa,QAAO,cAAc,IAAI;AAC1D,QAAM,KAAK,KAAK,MAAM,EAAE,eAAe,EAAE;AACzC,QAAM,KAAK,KAAK,MAAM,EAAE,eAAe,EAAE;AACzC,QAAM,SAAS,OAAO,SAAS,EAAE;AACjC,QAAM,SAAS,OAAO,SAAS,EAAE;AACjC,MAAI,UAAU,UAAU,OAAO,GAAI,QAAO,KAAK;AAC/C,MAAI,WAAW,OAAQ,QAAO,SAAS,KAAK;AAC5C,QAAM,QAAQ,iBAAiB,CAAC,EAAE,cAAc,iBAAiB,CAAC,CAAC;AACnE,MAAI,UAAU,EAAG,QAAO;AACxB,SAAO,EAAE,UAAU,cAAc,EAAE,SAAS;AAC9C;AAEA,eAAsB,yBACpB,QACA,SACmC;AACnC,QAAM,eAAe,QAAQ,OAAO,oBAAI,KAAK,GAAG,YAAY;AAM5D,QAAM,aAAa,6BAA6B,MAAM,EAAE,aACpD,wBAAwB,MAAM,IAC9B,CAAC,OAAO,iBAAiB,KAAK,CAAC,EAAE,OAAO,OAAO;AACnD,QAAM,cAAc,oBAAI,IAA2C;AACnE,QAAM,UAAkD,CAAC;AAEzD,aAAW,aAAa,YAAY;AAClC,UAAM,OAAO,oBAAoB,QAAQ,SAAS;AAClD,gBAAY,IAAI,WAAW;AAAA,MACzB;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,IACV,CAAC;AAAA,EACH;AAEA,MAAI,6BAA6B,MAAM,EAAE,cAAc,uCAAuC,MAAM,EAAE,+BAA+B,OAAO;AAC1I,UAAM,gBAAgB,IAAI,IAAI,UAAU;AACxC,QAAI;AACF,YAAM,UAAU,QAAQ,SAAS,UAAU,MAAM,QAAQ,QAAQ,eAAe,IAAI,CAAC;AACrF,iBAAW,UAAU,SAAS;AAC5B,cAAM,YAAY,OAAO,UAAU,KAAK;AACxC,YAAI,CAAC,UAAW;AAChB,cAAM,eAAe,cAAc,IAAI,SAAS;AAChD,cAAM,OAAO,eAAe,oBAAoB,QAAQ,SAAS,IAAI,OAAO;AAC5E,YAAI,CAAC,qBAAqB,QAAQ,IAAI,GAAG;AACvC,kBAAQ,KAAK;AAAA,YACX;AAAA,YACA;AAAA,YACA,QAAQ,sBAAsB,IAAI;AAAA,UACpC,CAAC;AACD;AAAA,QACF;AACA,YAAI,CAAC,gBAAgB,CAAE,MAAM,kBAAkB,QAAQ,MAAM,GAAI;AAC/D,kBAAQ,KAAK;AAAA,YACX;AAAA,YACA;AAAA,YACA,QAAQ;AAAA,UACV,CAAC;AACD;AAAA,QACF;AACA,oBAAY,IAAI,WAAW;AAAA,UACzB;AAAA,UACA;AAAA,UACA,YAAY,OAAO;AAAA,UACnB,QAAQ,eAAe,eAAe;AAAA,UACtC,aAAa,OAAO;AAAA,UACpB,mBAAmB,OAAO,oBAAoB,QAAQ,OAAO;AAAA,QAC/D,CAAC;AAAA,MACH;AAAA,IACF,SAAS,OAAO;AACd,cAAQ,KAAK;AAAA,QACX,WAAW;AAAA,QACX,QAAQ;AAAA,QACR,QAAQ,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,MAC/D,CAAC;AAAA,IACH;AAAA,EACF,WAAW,6BAA6B,MAAM,EAAE,YAAY;AAC1D,YAAQ,KAAK;AAAA,MACX,WAAW;AAAA,MACX,QAAQ;AAAA,IACV,CAAC;AAAA,EACH;AAEA,MAAI,QAAQ,eAAe,aAAa;AACtC,UAAM,4BAA4B,MAAM,8BAA8B,QAAQ,QAAQ,OAAO;AAC7F,eAAW,aAAa,YAAY,OAAO,GAAG;AAC5C,UAAI,CAAC,UAAU,mBAAmB;AAChC,kBAAU,oBAAoB,0BAA0B,IAAI,UAAU,SAAS;AAAA,MACjF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,aAAa,CAAC,GAAG,YAAY,OAAO,CAAC,EACxC,OAAO,CAAC,cAAc,qBAAqB,QAAQ,UAAU,IAAI,CAAC,EAClE,KAAK,cAAc;AAEtB,QAAM,MAAM,sBAAsB,MAAM;AACxC,QAAM,mBAAmB,QAAQ,eAAe;AAChD,QAAM,WAAW,mBAAmB,WAAW,MAAM,GAAG,GAAG,IAAI;AAC/D,MAAI,kBAAkB;AACpB,eAAW,aAAa,WAAW,MAAM,GAAG,GAAG;AAC7C,cAAQ,KAAK;AAAA,QACX,WAAW,UAAU;AAAA,QACrB,MAAM,UAAU;AAAA,QAChB,QAAQ;AAAA,MACV,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AAAA,IACL,SAAS,QAAQ;AAAA,IACjB;AAAA,IACA,YAAY;AAAA,IACZ;AAAA,IACA,QAAQ;AAAA,MACN,uBAAuB;AAAA,MACvB,UAAU,SAAS;AAAA,IACrB;AAAA,EACF;AACF;AAEA,SAAS,kBAAkB,OAAuB;AAChD,QAAM,YAAY,MAAM,QAAQ,oBAAoB,GAAG,EAAE,MAAM,GAAG,GAAG,KAAK;AAC1E,MAAI,UAAU,UAAU,OAAO,cAAc,MAAO,QAAO;AAC3D,SAAO,GAAG,UAAU,MAAM,GAAG,EAAE,CAAC,IAAI,WAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK,EAAE,MAAM,GAAG,EAAE,CAAC;AACnG;AAEA,SAAS,qBAAqB,WAA2B;AACvD,QAAM,QAAQ,uBAAuB,SAAS;AAC9C,MAAI,MAAM,UAAU,IAAK,QAAO;AAChC,SAAO,MAAM,WAAW,QAAQ,EAAE,OAAO,SAAS,EAAE,OAAO,KAAK,CAAC;AACnE;AAEA,SAAS,SAAS,QAAsB,SAAiB,WAA2B;AAClF,SAAO,KAAK;AAAA,IACV,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA,kBAAkB,OAAO;AAAA,IACzB,GAAG,qBAAqB,SAAS,CAAC;AAAA,EACpC;AACF;AAEA,SAAS,gCAAgC,QAA8B;AACrE,MACE,OAAO,OAAO,oCAAoC,YAClD,OAAO,SAAS,OAAO,+BAA+B,KACtD,OAAO,kCAAkC,GACzC;AACA,WAAO,KAAK,MAAM,OAAO,+BAA+B;AAAA,EAC1D;AACA,SAAO;AACT;AAEA,SAAS,oCAAoC,QAA8B;AACzE,QAAM,UAAU,gCAAgC,MAAM;AACtD,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,KAAQ,KAAK,MAAM,UAAU,CAAC,KAAK,CAAC,CAAC;AACnE;AAEA,SAAS,UAAU,OAAoC;AACrD,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,UAAU,QAC3D,MAA4B,OAC7B;AACN;AAEA,eAAe,sCACb,QACA,OACA,MACY;AACZ,QAAM,cAAc,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;AACzD,QAAM,WAAW,YAAY,MAAM;AACjC,eAAW,QAAQ,aAAa;AAC9B,WAAK,KAAK,MAAM,EAAE,MAAM,MAAM,MAAS;AAAA,IACzC;AAAA,EACF,GAAG,oCAAoC,MAAM,CAAC;AAC9C,WAAS,QAAQ;AACjB,MAAI;AACF,WAAO,MAAM,KAAK;AAAA,EACpB,UAAE;AACA,kBAAc,QAAQ;AAAA,EACxB;AACF;AAEA,eAAe,yBAAyB,UAAiC;AACvE,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,QAAQ,UAAU,EAAE,eAAe,KAAK,CAAC;AAAA,EAC3D,SAAS,OAAO;AACd,QAAI,UAAU,KAAK,MAAM,SAAU;AACnC,UAAM;AAAA,EACR;AACA,aAAW,SAAS,SAAS;AAC3B,QAAI,CAAC,MAAM,OAAO,EAAG;AACrB,UAAM,uBAAuB,GAAG,KAAK,KAAK,UAAU,MAAM,IAAI,GAAG,EAAE,OAAO,KAAK,CAAC;AAAA,EAClF;AACA,MAAI;AACF,UAAM,MAAM,QAAQ;AAAA,EACtB,SAAS,OAAO;AACd,QAAI,UAAU,KAAK,MAAM,SAAU;AACnC,UAAM;AAAA,EACR;AACF;AAEA,eAAe,mCACb,QACA,SACA,WAC4B;AAC5B,QAAM,WAAW,SAAS,QAAQ,SAAS,SAAS;AACpD,QAAM,MAAM,KAAK,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AACvD,MAAI;AACF,UAAM,SAAS,WAAW;AAC1B,UAAM,MAAM,QAAQ;AACpB,UAAM,YAAY,KAAK,KAAK,UAAU,GAAG,MAAM,OAAO;AACtD,QAAI;AACJ,QAAI;AACF,eAAS,MAAM,uBAAuB,KAAK,WAAW,IAAI;AAC1D,YAAM,OAAO;AAAA,QACX,GAAG,KAAK,UAAU;AAAA,UAChB;AAAA,UACA,KAAK,QAAQ;AAAA,UACb;AAAA,UACA;AAAA,UACA,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,QACrC,CAAC,CAAC;AAAA;AAAA,QACF;AAAA,MACF;AACA,YAAM,OAAO,MAAM;AAAA,IACrB,SAAS,YAAY;AACnB,YAAM,QAAQ,MAAM,EAAE,MAAM,MAAM,MAAS;AAC3C,YAAM,uBAAuB,GAAG,WAAW,EAAE,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM,MAAS;AACjF,YAAM,MAAM,QAAQ,EAAE,MAAM,MAAM,MAAS;AAC3C,YAAM;AAAA,IACR;AACA,WAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAM,QAAQ;AACZ,YAAI;AACF,gBAAM,SAAS,KAAK,MAAM,MAAM,SAAS,WAAW,MAAM,CAAC;AAC3D,cAAI,OAAO,WAAW,QAAQ;AAC5B,kBAAM,MAAM,oBAAI,KAAK;AACrB,kBAAM,OAAO,WAAW,KAAK,GAAG;AAChC,kBAAM,OAAO,UAAU,KAAK,GAAG;AAAA,UACjC;AAAA,QACF,QAAQ;AAAA,QAAC;AAAA,MACX;AAAA,MACA,MAAM,UAAU;AACd,YAAI;AACF,gBAAM,SAAS,KAAK,MAAM,MAAM,SAAS,WAAW,MAAM,CAAC;AAC3D,cAAI,OAAO,WAAW,QAAQ;AAC5B,kBAAM,uBAAuB,GAAG,WAAW,EAAE,OAAO,KAAK,CAAC;AAC1D,kBAAM,MAAM,QAAQ,EAAE,MAAM,MAAM,MAAS;AAAA,UAC7C;AAAA,QACF,QAAQ;AAAA,QAAC;AAAA,MACX;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AACd,QAAI,UAAU,KAAK,MAAM,UAAU;AACjC,YAAM,UACJ,gCAAgC,MAAM;AACxC,UAAI;AACF,cAAM,IAAI,MAAM,MAAM,QAAQ;AAC9B,YAAI,EAAE,eAAe,GAAG;AACtB,iBAAO;AAAA,QACT;AACA,aAAK,EAAE,OAAO,KAAK,EAAE,YAAY,MAAM,KAAK,IAAI,IAAI,EAAE,UAAU,SAAS;AACvE,cAAI;AACF,gBAAI,EAAE,YAAY,GAAG;AACnB,oBAAM,yBAAyB,QAAQ;AAAA,YACzC,OAAO;AACL,oBAAM,uBAAuB,GAAG,UAAU,EAAE,OAAO,KAAK,CAAC;AAAA,YAC3D;AAAA,UACF,SAAS,aAAa;AACpB,gBAAI,UAAU,WAAW,MAAM,UAAU;AACvC,qBAAO,mCAAmC,QAAQ,SAAS,SAAS;AAAA,YACtE;AACA,gBAAI,UAAU,WAAW,MAAM,aAAa;AAC1C,qBAAO;AAAA,YACT;AACA,kBAAM;AAAA,UACR;AACA,iBAAO,mCAAmC,QAAQ,SAAS,SAAS;AAAA,QACtE;AAAA,MACF,SAAS,WAAW;AAClB,YAAI,UAAU,SAAS,MAAM,UAAU;AACrC,iBAAO,mCAAmC,QAAQ,SAAS,SAAS;AAAA,QACtE;AACA,cAAM;AAAA,MACR;AACA,aAAO;AAAA,IACT;AACA,UAAM;AAAA,EACR;AACF;AAEA,SAAS,eAAe,QAA8B;AACpD,SAAO,KAAK,KAAK,OAAO,WAAW,SAAS,WAAW;AACzD;AAEA,SAAS,WAAW,QAAsB,SAAiB,WAA2B;AACpF,SAAO,KAAK,KAAK,eAAe,MAAM,GAAG,kBAAkB,OAAO,GAAG,GAAG,qBAAqB,SAAS,CAAC,OAAO;AAChH;AAEA,SAAS,kBAAkB,QAAsB,SAAiB,WAA2B;AAC3F,SAAO,KAAK,KAAK,eAAe,MAAM,GAAG,kBAAkB,OAAO,GAAG,GAAG,qBAAqB,SAAS,CAAC,gBAAgB;AACzH;AAEA,SAAS,YAAY,OAAsD;AACzE,MAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,EAAG,QAAO;AACxE,QAAM,IAAI;AACV,MACE,OAAO,EAAE,cAAc,YACvB,OAAO,EAAE,YAAY,aACpB,EAAE,UAAU,SAAS,EAAE,UAAU,aAAa,EAAE,UAAU,aAC3D,OAAO,EAAE,cAAc,YACvB,OAAO,EAAE,gBAAgB,UACzB;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,eAAe,8BAA8B,QAAsB,SAA+C;AAChH,QAAM,SAAS,oBAAI,IAAoB;AACvC,QAAM,WAAW,oBAAI,IAAoB;AACzC,aAAW,UAAU,CAAC,GAAI,MAAM,gBAAgB,MAAM,GAAI,GAAI,MAAM,uBAAuB,MAAM,CAAE,GAAG;AACpG,QAAI,OAAO,UAAU,MAAO;AAC5B,QAAI,OAAO,YAAY,QAAS;AAChC,UAAM,gBAAgB,KAAK,MAAM,OAAO,WAAW;AACnD,QAAI,CAAC,OAAO,SAAS,aAAa,EAAG;AACrC,UAAM,aAAa,SAAS,IAAI,OAAO,SAAS;AAChD,QAAI,eAAe,UAAa,cAAc,cAAe;AAC7D,aAAS,IAAI,OAAO,WAAW,aAAa;AAC5C,WAAO,IAAI,OAAO,WAAW,OAAO,WAAW;AAAA,EACjD;AACA,SAAO;AACT;AAEA,eAAe,eAAe,UAAiE;AAC7F,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,MAAM,SAAS,UAAU,MAAM,CAAC;AAC1D,WAAO,YAAY,MAAM;AAAA,EAC3B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAe,gBAAgB,QAAgE;AAC7F,MAAI,OAAO,OAAO,cAAc,YAAY,OAAO,UAAU,WAAW,GAAG;AACzE,WAAO,CAAC;AAAA,EACV;AACA,QAAM,OAAO,eAAe,MAAM;AAClC,QAAM,WAA4C,CAAC;AACnD,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,QAAQ,MAAM,EAAE,eAAe,KAAK,CAAC;AAAA,EACvD,QAAQ;AACN,WAAO;AAAA,EACT;AACA,aAAW,UAAU,SAAS;AAC5B,QAAI,CAAC,OAAO,YAAY,EAAG;AAC3B,QAAI;AACJ,QAAI;AACF,cAAQ,MAAM,QAAQ,KAAK,KAAK,MAAM,OAAO,IAAI,GAAG,EAAE,eAAe,KAAK,CAAC;AAAA,IAC7E,QAAQ;AACN;AAAA,IACF;AACA,eAAW,QAAQ,OAAO;AACxB,UAAI,CAAC,KAAK,OAAO,KAAK,CAAC,KAAK,KAAK,SAAS,OAAO,EAAG;AACpD,UAAI,KAAK,KAAK,SAAS,gBAAgB,EAAG;AAC1C,YAAM,SAAS,MAAM,eAAe,KAAK,KAAK,MAAM,OAAO,MAAM,KAAK,IAAI,CAAC;AAC3E,UAAI,OAAQ,UAAS,KAAK,MAAM;AAAA,IAClC;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAe,uBAAuB,QAAgE;AACpG,MAAI,OAAO,OAAO,cAAc,YAAY,OAAO,UAAU,WAAW,GAAG;AACzE,WAAO,CAAC;AAAA,EACV;AACA,QAAM,OAAO,eAAe,MAAM;AAClC,QAAM,WAA4C,CAAC;AACnD,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,QAAQ,MAAM,EAAE,eAAe,KAAK,CAAC;AAAA,EACvD,QAAQ;AACN,WAAO;AAAA,EACT;AACA,aAAW,UAAU,SAAS;AAC5B,QAAI,CAAC,OAAO,YAAY,EAAG;AAC3B,QAAI;AACJ,QAAI;AACF,cAAQ,MAAM,QAAQ,KAAK,KAAK,MAAM,OAAO,IAAI,GAAG,EAAE,eAAe,KAAK,CAAC;AAAA,IAC7E,QAAQ;AACN;AAAA,IACF;AACA,eAAW,QAAQ,OAAO;AACxB,UAAI,CAAC,KAAK,OAAO,KAAK,CAAC,KAAK,KAAK,SAAS,gBAAgB,EAAG;AAC7D,YAAM,SAAS,MAAM,eAAe,KAAK,KAAK,MAAM,OAAO,MAAM,KAAK,IAAI,CAAC;AAC3E,UAAI,OAAQ,UAAS,KAAK,MAAM;AAAA,IAClC;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAe,mBAAmB,QAAgB,QAAsD;AACtG,QAAM,MAAM,KAAK,QAAQ,MAAM;AAC/B,QAAM,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC;AACpC,QAAM,OAAO,GAAG,MAAM,IAAI,QAAQ,GAAG,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,CAAC,CAAC;AAC1F,QAAM,UAAU;AAAA,IACd,SAAS;AAAA,IACT,GAAG;AAAA,EACL;AACA,QAAM,UAAU,MAAM,GAAG,KAAK,UAAU,SAAS,MAAM,CAAC,CAAC;AAAA,GAAM,MAAM;AACrE,QAAM,OAAO,MAAM,MAAM;AAC3B;AAEA,eAAe,gBAAgB,QAAsB,QAAsD;AACzG,QAAM,mBAAmB,WAAW,QAAQ,OAAO,SAAS,OAAO,SAAS,GAAG,MAAM;AACrF,MAAI,OAAO,UAAU,OAAO;AAC1B,UAAM,mBAAmB,kBAAkB,QAAQ,OAAO,SAAS,OAAO,SAAS,GAAG,MAAM;AAAA,EAC9F;AACF;AAEA,eAAe,uCACb,QACA,QACe;AACf,MAAI;AACF,UAAM,gBAAgB,QAAQ,MAAM;AAAA,EACtC,QAAQ;AAAA,EAER;AACF;AAEA,SAAS,uBAAuB,OAAwB;AACtD,SAAO,mBAAmB,KAAK,KAAK;AACtC;AAEA,eAAsB,iCAAiC,QAAgE;AACrH,UAAQ,MAAM,gBAAgB,MAAM,GAAG,KAAK,CAAC,GAAG,MAAM;AACpD,UAAM,QAAQ,EAAE,QAAQ,cAAc,EAAE,OAAO;AAC/C,QAAI,UAAU,EAAG,QAAO;AACxB,WAAO,EAAE,UAAU,cAAc,EAAE,SAAS;AAAA,EAC9C,CAAC;AACH;AAYA,eAAsB,wCACpB,QAC0C;AAC1C,UAAQ,MAAM,uBAAuB,MAAM,GAAG,KAAK,CAAC,GAAG,MAAM;AAC3D,UAAM,QAAQ,EAAE,QAAQ,cAAc,EAAE,OAAO;AAC/C,QAAI,UAAU,EAAG,QAAO;AACxB,WAAO,EAAE,UAAU,cAAc,EAAE,SAAS;AAAA,EAC9C,CAAC;AACH;AAgBA,eAAsB,4BACpB,QACA,MACA,QACA,SACsC;AACtC,QAAM,WAA4C,CAAC;AAEnD,aAAW,WAAW,KAAK,SAAS;AAClC,QAAI,QAAQ,cAAc,IAAK;AAC/B,UAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,UAAM,SAAwC;AAAA,MAC5C,WAAW,QAAQ;AAAA,MACnB,SAAS,KAAK;AAAA,MACd,OAAO;AAAA,MACP,QAAQ,QAAQ;AAAA,MAChB,WAAW;AAAA,MACX,aAAa;AAAA,IACf;AACA,aAAS,KAAK,MAAM;AACpB,UAAM,uCAAuC,QAAQ,MAAM;AAAA,EAC7D;AAEA,aAAW,aAAa,KAAK,YAAY;AACvC,UAAM,aAAY,oBAAI,KAAK,GAAE,YAAY;AACzC,UAAM,OAAO,MAAM,mCAAmC,QAAQ,KAAK,SAAS,UAAU,SAAS;AAC/F,QAAI,CAAC,MAAM;AACT,YAAM,eAAc,oBAAI,KAAK,GAAE,YAAY;AAC3C,YAAM,SAAwC;AAAA,QAC5C,WAAW,UAAU;AAAA,QACrB,SAAS,KAAK;AAAA,QACd,OAAO;AAAA,QACP,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,MACF;AACA,eAAS,KAAK,MAAM;AACpB,YAAM,uCAAuC,QAAQ,MAAM;AAC3D;AAAA,IACF;AACA,QAAI;AACF,YAAM,SAAS,MAAM,sCAAsC,QAAQ,MAAM,MAAM,OAAO,SAAS,CAAC;AAChG,YAAM,eAAc,oBAAI,KAAK,GAAE,YAAY;AAC3C,UAAI,QAAQ,SAAS;AAKnB,cAAM,SAAwC;AAAA,UAC5C,WAAW,UAAU;AAAA,UACrB,SAAS,KAAK;AAAA,UACd,OAAO;AAAA,UACP,QAAS,OAAO,cAAc;AAAA,UAC9B;AAAA,UACA;AAAA,QACF;AACA,iBAAS,KAAK,MAAM;AACpB,cAAM,uCAAuC,QAAQ,MAAM;AAAA,MAC7D,OAAO;AACL,cAAM,SAAwC;AAAA,UAC5C,WAAW,UAAU;AAAA,UACrB,SAAS,KAAK;AAAA,UACd,OAAO;AAAA,UACP;AAAA,UACA;AAAA,UACA,WAAW,QAAQ;AAAA,QACrB;AACA,iBAAS,KAAK,MAAM;AACpB,cAAM,uCAAuC,QAAQ,MAAM;AAC3D,YAAI;AACF,gBAAM,SAAS,gBAAgB,UAAU,WAAW,KAAK,SAAS,IAAI,KAAK,WAAW,CAAC;AAAA,QACzF,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AACd,YAAM,eAAc,oBAAI,KAAK,GAAE,YAAY;AAC3C,YAAM,SAAwC;AAAA,QAC5C,WAAW,UAAU;AAAA,QACrB,SAAS,KAAK;AAAA,QACd,OAAO;AAAA,QACP,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACA,OAAO,uBAAuB,KAAK;AAAA,MACrC;AACA,eAAS,KAAK,MAAM;AACpB,YAAM,uCAAuC,QAAQ,MAAM;AAAA,IAC7D,UAAE;AACA,YAAM,KAAK,QAAQ,EAAE,MAAM,MAAM,MAAS;AAAA,IAC5C;AAAA,EACF;AAEA,SAAO;AAAA,IACL,SAAS,KAAK;AAAA,IACd,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC,KAAK,SAAS,OAAO,CAAC,MAAM,EAAE,UAAU,KAAK,EAAE;AAAA,IAC/C,SAAS,SAAS,OAAO,CAAC,MAAM,EAAE,UAAU,SAAS,EAAE;AAAA,IACvD,QAAQ,SAAS,OAAO,CAAC,MAAM,EAAE,UAAU,QAAQ,EAAE;AAAA,IACrD;AAAA,EACF;AACF;AAEA,eAAsB,iCACpB,QACA,MACA,QACA,SACA,UAA+C,CAAC,GACV;AACtC,QAAM,WAA4C,CAAC;AAEnD,aAAW,WAAW,KAAK,SAAS;AAClC,QAAI,QAAQ,cAAc,IAAK;AAC/B,UAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,UAAM,SAAwC;AAAA,MAC5C,WAAW,QAAQ;AAAA,MACnB,SAAS,KAAK;AAAA,MACd,OAAO;AAAA,MACP,QAAQ,QAAQ;AAAA,MAChB,WAAW;AAAA,MACX,aAAa;AAAA,IACf;AACA,aAAS,KAAK,MAAM;AACpB,UAAM,uCAAuC,QAAQ,MAAM;AAAA,EAC7D;AAEA,QAAM,WAID,CAAC;AAEN,MAAI;AACF,eAAW,aAAa,KAAK,YAAY;AACvC,YAAM,aAAY,oBAAI,KAAK,GAAE,YAAY;AACzC,YAAM,OAAO,MAAM,mCAAmC,QAAQ,KAAK,SAAS,UAAU,SAAS;AAC/F,UAAI,CAAC,MAAM;AACT,cAAM,eAAc,oBAAI,KAAK,GAAE,YAAY;AAC3C,cAAM,SAAwC;AAAA,UAC5C,WAAW,UAAU;AAAA,UACrB,SAAS,KAAK;AAAA,UACd,OAAO;AAAA,UACP,QAAQ;AAAA,UACR;AAAA,UACA;AAAA,QACF;AACA,iBAAS,KAAK,MAAM;AACpB,cAAM,uCAAuC,QAAQ,MAAM;AAC3D;AAAA,MACF;AACA,eAAS,KAAK,EAAE,WAAW,MAAM,UAAU,CAAC;AAAA,IAC9C;AAAA,EACF,SAAS,OAAO;AACd,UAAM,QAAQ,IAAI,SAAS,IAAI,CAAC,EAAE,KAAK,MAAM,KAAK,QAAQ,EAAE,MAAM,MAAM,MAAS,CAAC,CAAC;AACnF,UAAM;AAAA,EACR;AAEA,MAAI,QAAQ,mBAAmB,SAAS,SAAS,KAAK,SAAS,SAAS,KAAK,WAAW,QAAQ;AAC9F,eAAW,EAAE,WAAW,UAAU,KAAK,UAAU;AAC/C,YAAM,eAAc,oBAAI,KAAK,GAAE,YAAY;AAC3C,YAAM,SAAwC;AAAA,QAC5C,WAAW,UAAU;AAAA,QACrB,SAAS,KAAK;AAAA,QACd,OAAO;AAAA,QACP,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,MACF;AACA,eAAS,KAAK,MAAM;AACpB,YAAM,uCAAuC,QAAQ,MAAM;AAAA,IAC7D;AACA,UAAM,QAAQ,IAAI,SAAS,IAAI,CAAC,EAAE,KAAK,MAAM,KAAK,QAAQ,EAAE,MAAM,MAAM,MAAS,CAAC,CAAC;AACnF,WAAO;AAAA,MACL,SAAS,KAAK;AAAA,MACd,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,MACpC,KAAK,SAAS,OAAO,CAAC,MAAM,EAAE,UAAU,KAAK,EAAE;AAAA,MAC/C,SAAS,SAAS,OAAO,CAAC,MAAM,EAAE,UAAU,SAAS,EAAE;AAAA,MACvD,QAAQ,SAAS,OAAO,CAAC,MAAM,EAAE,UAAU,QAAQ,EAAE;AAAA,MACrD;AAAA,IACF;AAAA,EACF;AAEA,MAAI,SAAS,WAAW,GAAG;AACzB,WAAO;AAAA,MACL,SAAS,KAAK;AAAA,MACd,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,MACpC,KAAK,SAAS,OAAO,CAAC,MAAM,EAAE,UAAU,KAAK,EAAE;AAAA,MAC/C,SAAS,SAAS,OAAO,CAAC,MAAM,EAAE,UAAU,SAAS,EAAE;AAAA,MACvD,QAAQ,SAAS,OAAO,CAAC,MAAM,EAAE,UAAU,QAAQ,EAAE;AAAA,MACrD;AAAA,IACF;AAAA,EACF;AAEA,MAAI;AACF,UAAM,SAAS,MAAM;AAAA,MACnB;AAAA,MACA,SAAS,IAAI,CAAC,EAAE,KAAK,MAAM,IAAI;AAAA,MAC/B,MAAM,OAAO,SAAS,IAAI,CAAC,EAAE,UAAU,MAAM,SAAS,CAAC;AAAA,IACzD;AACA,eAAW,EAAE,WAAW,UAAU,KAAK,UAAU;AAC/C,YAAM,eAAc,oBAAI,KAAK,GAAE,YAAY;AAC3C,YAAM,SAAwC;AAAA,QAC5C,WAAW,UAAU;AAAA,QACrB,SAAS,KAAK;AAAA,QACd,OAAO;AAAA,QACP;AAAA,QACA;AAAA,QACA,WAAW,sBAAsB,QAAQ,UAAU,SAAS;AAAA,MAC9D;AACA,eAAS,KAAK,MAAM;AACpB,YAAM,uCAAuC,QAAQ,MAAM;AAC3D,UAAI;AACF,cAAM,SAAS,gBAAgB,UAAU,WAAW,KAAK,SAAS,IAAI,KAAK,WAAW,CAAC;AAAA,MACzF,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AACd,UAAM,aAAa,QAAQ,qBAAqB,KAAK;AACrD,eAAW,EAAE,WAAW,UAAU,KAAK,UAAU;AAC/C,YAAM,eAAc,oBAAI,KAAK,GAAE,YAAY;AAC3C,YAAM,SAAwC,aAC1C;AAAA,QACE,WAAW,UAAU;AAAA,QACrB,SAAS,KAAK;AAAA,QACd,OAAO;AAAA,QACP,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,MACF,IACA;AAAA,QACE,WAAW,UAAU;AAAA,QACrB,SAAS,KAAK;AAAA,QACd,OAAO;AAAA,QACP,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACA,OAAO,uBAAuB,KAAK;AAAA,MACrC;AACJ,eAAS,KAAK,MAAM;AACpB,YAAM,uCAAuC,QAAQ,MAAM;AAAA,IAC7D;AAAA,EACF,UAAE;AACA,UAAM,QAAQ,IAAI,SAAS,IAAI,CAAC,EAAE,KAAK,MAAM,KAAK,QAAQ,EAAE,MAAM,MAAM,MAAS,CAAC,CAAC;AAAA,EACrF;AAEA,SAAO;AAAA,IACL,SAAS,KAAK;AAAA,IACd,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC,KAAK,SAAS,OAAO,CAAC,MAAM,EAAE,UAAU,KAAK,EAAE;AAAA,IAC/C,SAAS,SAAS,OAAO,CAAC,MAAM,EAAE,UAAU,SAAS,EAAE;AAAA,IACvD,QAAQ,SAAS,OAAO,CAAC,MAAM,EAAE,UAAU,QAAQ,EAAE;AAAA,IACrD;AAAA,EACF;AACF;AAEA,SAAS,sBACP,QACA,WACoB;AACpB,QAAM,aAAa,QAAQ;AAC3B,MAAI,sBAAsB,IAAK,QAAO,WAAW,IAAI,SAAS;AAC9D,MAAI,cAAc,OAAO,UAAU,eAAe,KAAK,YAAY,SAAS,GAAG;AAC7E,WAAO,WAAW,SAAS;AAAA,EAC7B;AACA,SAAO,QAAQ;AACjB;","names":[]}