@objectstack/service-analytics 17.0.0-rc.0 → 17.0.0-rc.2
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/CHANGELOG.md +3093 -0
- package/dist/index.cjs +615 -172
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +258 -15
- package/dist/index.d.ts +258 -15
- package/dist/index.js +606 -164
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/analytics-service.ts","../src/cube-registry.ts","../src/strategies/filter-normalizer.ts","../src/read-scope-sql.ts","../src/strategies/native-sql-strategy.ts","../src/strategies/cross-object-rebucket.ts","../src/strategies/objectql-strategy.ts","../src/dataset-compiler.ts","../src/dataset-executor.ts","../src/dimension-labels.ts","../src/preview-evaluator.ts","../src/plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n IAnalyticsService,\n AnalyticsQuery,\n AnalyticsResult,\n CubeMeta,\n DatasetSelection,\n} from '@objectstack/spec/contracts';\nimport type { Cube, FilterCondition } from '@objectstack/spec/data';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\nimport type { Dataset } from '@objectstack/spec/ui';\nimport type { Logger } from '@objectstack/spec/contracts';\nimport { createLogger, bucketKeyToCalendarRange, zonedDateStartToUtcMs } from '@objectstack/core';\nimport { CubeRegistry } from './cube-registry.js';\nimport type { AnalyticsStrategy, DriverCapabilities, StrategyContext } from './strategies/types.js';\nimport { NativeSQLStrategy } from './strategies/native-sql-strategy.js';\nimport { ObjectQLStrategy } from './strategies/objectql-strategy.js';\nimport { compileDataset, type CompiledDataset, type RelationshipResolver } from './dataset-compiler.js';\nimport { DatasetExecutor, resolveDimensionGranularity, type DateGranularityValue } from './dataset-executor.js';\nimport {\n resolveDimensionLabels,\n createOrderLabelResolver,\n withLabelFetchCache,\n type DimensionLabelDeps,\n} from './dimension-labels.js';\nimport { evaluateAnalyticsQueryOverRows } from './preview-evaluator.js';\n\n/**\n * Analytics result augmented with drill-through metadata (ADR-0021 D2; see\n * queryDataset). Carried alongside `rows` so the host can drill a clicked bucket\n * back to the underlying records without the renderer knowing field mappings.\n */\ntype AnalyticsResultWithDrill = AnalyticsResult & {\n /** The dataset's base object — the host drills into its records. */\n object?: string;\n /** Selected drillable dimension NAME → underlying object FIELD name. */\n dimensionFields?: Record<string, string>;\n /**\n * RAW grouped values per row, aligned to `rows` by index — each a map of\n * drillable dimension NAME → stored value (BEFORE label resolution rewrote\n * `rows[i][dim]` to the display label). The exact-match drill filter is built\n * from these, never from the display labels.\n */\n drillRawRows?: Array<Record<string, unknown>>;\n /**\n * RAW grouped values for the totals/subtotal rows (#3214), the totals-side\n * companion to `drillRawRows`: `drillRawTotals[i]` aligns to `result.totals[i]`\n * and `drillRawTotals[i][j]` to `result.totals[i].rows[j]`. Each map holds that\n * grouping's DRILLABLE dimension NAME → stored value, snapshotted in the SAME\n * pre-label-resolution pass (the totals loop below overwrites a subtotal row's\n * dimension value with its display label just like the data rows). Restricted\n * to the drillable dims present in the grouping, so the grand-total grouping\n * (`[]`) contributes an empty map per row — which keeps the index alignment\n * intact and correctly drills the whole (unfiltered) object.\n */\n drillRawTotals?: Array<Array<Record<string, unknown>>>;\n /**\n * #1752 — half-open date-range drill scope per row, the RANGE companion to\n * `drillRawRows` (which handles equality dims). A time-bucketed date\n * dimension (`dateGranularity`) groups a SPAN of records into one bucket\n * (\"2026-Q2\"), so its drill needs `[gte, lt)`, not equality — the humanized\n * bucket can't be exact-matched (which is why date dims are excluded from\n * `dimensionFields`/`drillRawRows`). Aligned to `rows` by index; each entry\n * maps a drillable date-dimension NAME → `{ field, gte, lt }` with `gte`\n * inclusive and `lt` exclusive (bounds as `YYYY-MM-DD`). Present only for\n * buckets whose boundaries are unambiguous — a `datetime` field under a\n * non-UTC reference timezone is omitted (host drills an unscoped superset)\n * until instant-boundary support lands.\n */\n drillRanges?: Array<Record<string, { field: string; gte: string; lt: string }>>;\n};\n\n/**\n * Detect the \"backing object/table isn't present in this kernel\" class of\n * error so a dataset query can degrade to an empty result instead of failing\n * the widget with a 500. Matches the missing-relation signatures across the\n * drivers ObjectStack runs on (sqlite/libsql, postgres, mysql) plus the\n * framework's own unknown-object signal. Deliberately scoped to MISSING SOURCE\n * (table/object/relation) — not column/syntax errors, which stay hard failures\n * so real query bugs still surface.\n */\nfunction isMissingSourceError(err: unknown): boolean {\n const msg = String((err as { message?: unknown })?.message ?? err ?? '').toLowerCase();\n return (\n msg.includes('no such table') || // sqlite / libsql\n (msg.includes('relation') && msg.includes('does not exist')) || // postgres\n msg.includes(\"doesn't exist\") || // mysql (\"table ... doesn't exist\")\n msg.includes('not registered') || // framework: object not in registry\n msg.includes('unknown object') ||\n msg.includes('is not a registered object')\n );\n}\n\n/**\n * Configuration for AnalyticsService.\n */\nexport interface AnalyticsServiceConfig {\n /** Pre-defined cube definitions (from manifest). */\n cubes?: Cube[];\n /** Logger instance. */\n logger?: Logger;\n /**\n * Probe driver capabilities for the object that backs a cube.\n * The service calls this function to decide which strategy can handle a query.\n */\n queryCapabilities?: (cubeName: string) => DriverCapabilities;\n /**\n * Execute raw SQL on the driver for a given object.\n * Required for NativeSQLStrategy.\n */\n executeRawSql?: (objectName: string, sql: string, params: unknown[]) => Promise<Record<string, unknown>[]>;\n /**\n * Execute an ObjectQL aggregate query.\n * Required for ObjectQLStrategy.\n */\n executeAggregate?: (objectName: string, options: {\n groupBy?: string[];\n aggregations?: Array<{ field: string; method: string; alias: string }>;\n filter?: Record<string, unknown>;\n /** Reference timezone (IANA) for date bucketing — ADR-0053 Phase 2. */\n timezone?: string;\n /**\n * ADR-0021 D-C (#3602) — the request's ExecutionContext. Bridges MUST\n * forward it to `engine.aggregate` so engine-side RLS applies; see\n * `StrategyContext.executeAggregate` for why this is a second belt rather\n * than a replacement for `getReadScope`.\n */\n context?: ExecutionContext;\n }) => Promise<Record<string, unknown>[]>;\n /**\n * Fallback IAnalyticsService (e.g. MemoryAnalyticsService).\n * Used by InMemoryStrategy.\n */\n fallbackService?: IAnalyticsService;\n /**\n * Custom strategies to add/replace the defaults.\n * They are merged with the built-in strategies and sorted by priority.\n */\n strategies?: AnalyticsStrategy[];\n /**\n * ADR-0021 D-C — context-aware per-object read scope (tenant + RLS). Supplied\n * by the runtime that owns the sharing middleware; receives the current\n * request's ExecutionContext and returns the RLS `FilterCondition` for the\n * object (exactly what `RLSCompiler` emits). The service binds the active\n * context per query and the strategy compiles the filter into alias-qualified\n * SQL injected into every base and joined table.\n *\n * MAY be async: the production bridge resolves RLS from the `security`\n * service's `getReadFilter`, which can hit the database. The service\n * pre-resolves the scope for every base + joined object of a query (before\n * the synchronous SQL builder runs), so a sync return still works unchanged.\n */\n getReadScope?: (\n objectName: string,\n context?: ExecutionContext,\n ) =>\n | FilterCondition\n | null\n | undefined\n | Promise<FilterCondition | null | undefined>;\n /**\n * ADR-0021 D-C — join allowlist per cube (the dataset's declared `include`).\n * Joins outside this set are rejected by the strategy. Compiled datasets\n * (via `queryDataset`/`registerDataset`) supply this automatically; this\n * config hook is a fallback for legacy hand-authored cubes.\n */\n getAllowedRelationships?: (cubeName: string) => Set<string> | undefined;\n /**\n * Coerce a filter comparand to a temporal column's storage form so a\n * relative-date / ISO-string value compares correctly on the active driver\n * (SQLite `Field.datetime` → epoch ms; `Field.date` / native timestamp →\n * unchanged). Threaded into the StrategyContext and consulted by\n * `NativeSQLStrategy` when binding filter values. See the contract docs on\n * `StrategyContext.coerceTemporalFilterValue` for the full rationale.\n */\n coerceTemporalFilterValue?: (objectName: string, fieldName: string, value: unknown) => unknown;\n /**\n * ADR-0062 D6 — report whether an object is federated (external datasource).\n * Threaded into the StrategyContext so `NativeSQLStrategy` declines external\n * objects (which it would otherwise query against the wrong physical table),\n * routing them to the driver-correct ObjectQL aggregate path instead. See\n * `StrategyContext.isExternalObject`.\n */\n isExternalObject?: (objectName: string) => boolean;\n /**\n * [#3867] Is `name` a registered object in this kernel's schema registry?\n *\n * Consulted by {@link AnalyticsService.ensureCube} on the auto-inference\n * path only. When no Cube is registered under the queried name, the service\n * infers a minimal one whose `sql` IS that name — the intended \"metric over\n * an object\" path (an `object-metric` KPI widget queries `crm_account`\n * without anyone authoring a Cube). Without this hook that inference accepts\n * ANY string, so an arbitrary physical table name reached the driver: the\n * analytics-side twin of the data-path gap closed in #3770.\n *\n * Optional, and absence means \"skip the check\" — same tiering as #3770's\n * `assertObjectRegistered`: with no registry to consult the question cannot\n * be answered, and failing closed would break every embedding that runs\n * analytics without a data engine. The production bridge in `plugin.ts`\n * always wires it.\n */\n isRegisteredObject?: (name: string) => boolean;\n /**\n * ADR-0021 — optional object-graph resolver used when compiling datasets:\n * `(baseObject, relationshipName) => relatedObjectName | undefined`. When\n * provided, `queryDataset` validates that every declared `include` exists.\n */\n relationshipResolver?: RelationshipResolver;\n /**\n * ADR-0053 currency chain — resolve a measure's SOURCE FIELD currency\n * metadata so a monetary measure that omits an explicit `currency` falls back\n * to the field's declared currency, then the tenant default (`ctx.currency`).\n * Returns the source field's `type` and (fixed-mode) `defaultCurrency`;\n * `undefined` for an unknown field. Non-`currency` fields never get a code.\n */\n measureCurrency?: (object: string, field: string) => { type?: string; defaultCurrency?: string } | undefined;\n /** Pre-defined datasets to compile + register at construction (ADR-0021). */\n datasets?: Dataset[];\n /**\n * ADR-0021 — resolve raw dimension values to human display labels. When\n * provided, `queryDataset` post-processes result rows so a `select` dimension\n * shows its option label (not the stored value) and a `lookup`/`master_detail`\n * dimension shows the related record's display name (not the FK id). Injected\n * by the plugin from the `data` engine; omit to keep raw values.\n */\n labelResolver?: DimensionLabelDeps;\n\n /**\n * ADR-0037 Phase 3 — draft data preview. Resolve the PENDING `seed` draft\n * rows for an object (returns null when the object has no pending seed).\n * When provided and `queryDataset` is called with `previewDrafts`, the\n * selection is evaluated over these rows in memory instead of the engine —\n * the Live Canvas charts real numbers from the drafted sample data, and\n * because publish materializes the SAME seed, the numbers are continuous\n * across the publish boundary. Reads only; never touches physical tables.\n */\n draftRowsResolver?: (\n objectName: string,\n context?: ExecutionContext,\n ) => Promise<Record<string, unknown>[] | null>;\n}\n\n/**\n * Default capabilities when probing is not configured — assumes in-memory only.\n */\nconst DEFAULT_CAPABILITIES: DriverCapabilities = {\n nativeSql: false,\n objectqlAggregate: false,\n inMemory: true,\n};\n\n/**\n * AnalyticsService — Multi-driver analytics orchestrator.\n *\n * Implements `IAnalyticsService` by delegating to a priority-ordered\n * strategy chain:\n *\n * | Priority | Strategy | Condition |\n * |:---:|:---|:---|\n * | P1 (10) | NativeSQLStrategy | Driver supports raw SQL |\n * | P2 (20) | ObjectQLStrategy | Driver supports aggregate AST |\n * | P3 (30) | (custom / InMemoryStrategy from driver-memory) | Injected by user |\n *\n * When `fallbackService` is configured, an internal delegate strategy\n * is automatically appended at priority 30 as a safety net.\n *\n * The service also owns a `CubeRegistry` for metadata discovery and\n * auto-inference from object schemas.\n */\nexport class AnalyticsService implements IAnalyticsService {\n private readonly strategies: AnalyticsStrategy[];\n /** Context-independent part of the StrategyContext (no per-request scope). */\n private readonly baseCtx: StrategyContext;\n /** Context-aware read-scope provider (bound to the request's context per call). */\n private readonly readScopeProvider?: AnalyticsServiceConfig['getReadScope'];\n /** Compiled datasets by name — feeds the join allowlist (D-C) and queryDataset. */\n private readonly datasetRegistry = new Map<string, CompiledDataset>();\n /** Optional object-graph resolver used when compiling datasets. */\n private readonly relationshipResolver?: RelationshipResolver;\n private readonly measureCurrency?: AnalyticsServiceConfig['measureCurrency'];\n /** Optional dimension display-label resolver (select options / lookup names). */\n private readonly labelResolver?: DimensionLabelDeps;\n /** ADR-0037 P3: pending-seed row resolver for draft data preview. */\n private readonly draftRowsResolver?: AnalyticsServiceConfig['draftRowsResolver'];\n /** [#3867] Schema-registry probe gating cube auto-inference. */\n private readonly isRegisteredObject?: AnalyticsServiceConfig['isRegisteredObject'];\n /** [#3867] One-shot flag for the {@link assertInferableCube} stand-down warning. */\n private warnedNoObjectRegistry = false;\n readonly cubeRegistry: CubeRegistry;\n private readonly logger: Logger;\n\n constructor(config: AnalyticsServiceConfig = {}) {\n this.logger = config.logger || createLogger({ level: 'info', format: 'pretty' });\n this.cubeRegistry = new CubeRegistry();\n\n // Register pre-defined cubes\n if (config.cubes) {\n this.cubeRegistry.registerAll(config.cubes);\n }\n\n this.readScopeProvider = config.getReadScope;\n this.relationshipResolver = config.relationshipResolver;\n this.measureCurrency = config.measureCurrency;\n this.labelResolver = config.labelResolver;\n this.draftRowsResolver = config.draftRowsResolver;\n this.isRegisteredObject = config.isRegisteredObject;\n\n // Compile + register pre-defined datasets (ADR-0021).\n if (config.datasets) {\n for (const ds of config.datasets) {\n try {\n this.registerDataset(ds);\n } catch (e) {\n this.logger?.warn?.(`[Analytics] Failed to register dataset \"${ds?.name}\": ${String((e as Error)?.message ?? e)}`);\n }\n }\n }\n\n // Build the context-independent strategy context. `getReadScope` is bound\n // per query in `callCtx(context)` so it can resolve the active tenant.\n this.baseCtx = {\n getCube: (name) => this.cubeRegistry.get(name),\n queryCapabilities: config.queryCapabilities || (() => DEFAULT_CAPABILITIES),\n executeRawSql: config.executeRawSql,\n executeAggregate: config.executeAggregate,\n fallbackService: config.fallbackService,\n // Prefer a compiled dataset's declared relationships (D-C join allowlist);\n // fall back to any explicitly-configured provider for legacy cubes.\n getAllowedRelationships: (cubeName: string) =>\n this.datasetRegistry.get(cubeName)?.allowedRelationships\n ?? config.getAllowedRelationships?.(cubeName),\n coerceTemporalFilterValue: config.coerceTemporalFilterValue,\n isExternalObject: config.isExternalObject,\n };\n\n // Build strategy chain (built-in + custom, sorted by priority)\n // InMemoryStrategy is NOT built-in — it lives in @objectstack/driver-memory\n // and should be passed via config.strategies when needed.\n // When fallbackService is configured, an internal delegate is added at P3.\n const builtIn: AnalyticsStrategy[] = [\n new NativeSQLStrategy(),\n new ObjectQLStrategy(),\n ];\n\n // Auto-add fallback delegate when fallbackService is provided\n if (config.fallbackService) {\n builtIn.push(new FallbackDelegateStrategy());\n }\n\n const custom = config.strategies || [];\n this.strategies = [...builtIn, ...custom].sort((a, b) => a.priority - b.priority);\n\n this.logger.info(\n `[Analytics] Initialized with ${this.cubeRegistry.size} cubes, ` +\n `${this.strategies.length} strategies: ${this.strategies.map(s => s.name).join(' → ')}`,\n );\n }\n\n /**\n * Build a per-call StrategyContext that binds the read-scope provider to the\n * current request's ExecutionContext (ADR-0021 D-C). The strategy then sees a\n * `getReadScope(objectName)` that already knows the active tenant.\n */\n private async callCtx(\n query: AnalyticsQuery,\n context?: ExecutionContext,\n ): Promise<StrategyContext> {\n // #3602 — `context` rides along unconditionally. It is the ENGINE-side belt\n // (forwarded to `engine.aggregate`, where the middleware chain applies its\n // own RLS), so it must not be gated on the analytics-side belt being wired:\n // a deployment with no `getReadScope` provider is exactly the one that most\n // needs the engine to scope for it.\n if (!this.readScopeProvider) return { ...this.baseCtx, context };\n // Pre-resolve the read scope for every object the strategy will scan (base\n // + all declared joins) BEFORE the synchronous SQL builder runs, since the\n // provider may be async (the production `security.getReadFilter` bridge).\n // The strategy then reads each object's filter synchronously from the map.\n const scopes = await this.resolveReadScopes(query, context);\n return {\n ...this.baseCtx,\n context,\n getReadScope: (objectName: string) => scopes.get(objectName) ?? null,\n };\n }\n\n /**\n * Resolve the read scope (tenant + RLS `FilterCondition`) for the base object\n * AND every joined object of the query's cube, keyed by object name. This is\n * the async pre-pass that lets the synchronous strategy enforce scoping even\n * when the provider (security `getReadFilter`) resolves asynchronously.\n *\n * The object set is `cube.sql` (base) plus every `cube.joins[*].name` — a\n * SUPERSET of what the strategy actually scans (the strategy only joins along\n * declared relationships), so no scanned object is ever left unscoped.\n *\n * Fail-closed: if the provider throws for an object, the whole query is\n * rejected rather than emitting SQL with that object unscoped.\n */\n private async resolveReadScopes(\n query: AnalyticsQuery,\n context?: ExecutionContext,\n ): Promise<Map<string, FilterCondition>> {\n const map = new Map<string, FilterCondition>();\n const provider = this.readScopeProvider;\n if (!provider || !query.cube) return map;\n const cube = this.cubeRegistry.get(query.cube);\n if (!cube) return map;\n\n const objects = new Set<string>();\n if (typeof cube.sql === 'string' && cube.sql.trim()) {\n objects.add(cube.sql.trim());\n }\n const joins = (cube as { joins?: Record<string, { name?: string }> }).joins;\n if (joins) {\n for (const [alias, j] of Object.entries(joins)) {\n objects.add(j?.name ?? alias);\n }\n }\n\n for (const object of objects) {\n let filter: FilterCondition | null | undefined;\n try {\n filter = await provider(object, context);\n } catch (e) {\n // Deny the entire query — never fall through to unscoped SQL.\n this.logger.error?.(\n `[Analytics] read-scope resolution failed for object \"${object}\" — ` +\n `rejecting query (fail-closed, ADR-0021 D-C)`,\n e instanceof Error ? e : new Error(String(e)),\n );\n throw new Error(\n `[Analytics] read-scope resolution failed for \"${object}\"; query denied (fail-closed).`,\n );\n }\n if (filter != null) map.set(object, filter);\n }\n return map;\n }\n\n /**\n * Execute an analytical query by delegating to the first capable strategy.\n *\n * A strategy can discover only AT EXECUTION TIME that the underlying driver\n * cannot serve it — the canonical case is NativeSQLStrategy on an in-memory\n * driver, whose `execute()` returns null for raw SQL (the auto-bridge throws\n * `RAW_SQL_UNSUPPORTED`). That is a capability miss, not a query error: fall\n * back to the next capable strategy (e.g. ObjectQLStrategy over the\n * aggregate bridge) instead of failing — or worse, fabricating empty rows.\n * Any other error propagates untouched.\n */\n async query(query: AnalyticsQuery, context?: ExecutionContext): Promise<AnalyticsResult> {\n if (!query.cube) {\n throw new Error('Cube name is required in analytics query');\n }\n\n this.ensureCube(query);\n const ctx = await this.callCtx(query, context);\n let skip: Set<AnalyticsStrategy> | undefined;\n for (;;) {\n const strategy = this.resolveStrategy(query, ctx, skip);\n this.logger.debug(`[Analytics] Query on cube \"${query.cube}\" → ${strategy.name}`);\n try {\n return await strategy.execute(query, ctx);\n } catch (e) {\n if ((e as { code?: string })?.code === 'RAW_SQL_UNSUPPORTED') {\n this.logger.warn(\n `[Analytics] ${strategy.name} cannot run on this driver (raw SQL unsupported) — falling back to the next strategy.`,\n );\n (skip ??= new Set()).add(strategy);\n continue;\n }\n throw e;\n }\n }\n }\n\n /**\n * Compile a `dataset` (ADR-0021) and register its Cube + join allowlist so it\n * can be queried by name. Idempotent (re-registering overwrites). Returns the\n * compiled dataset.\n */\n registerDataset(dataset: Dataset): CompiledDataset {\n const compiled = compileDataset(dataset, this.relationshipResolver);\n this.cubeRegistry.register(compiled.cube);\n this.datasetRegistry.set(dataset.name, compiled);\n return compiled;\n }\n\n /**\n * Execute a semantic-layer dataset (ADR-0021). Compiles the dataset (saved or\n * inline draft — Studio preview), registers its Cube + join allowlist, then\n * runs the selection through the `DatasetExecutor` with the request context so\n * tenant/RLS scoping (D-C) is applied. See {@link IAnalyticsService.queryDataset}.\n */\n async queryDataset(\n dataset: Dataset,\n selection: DatasetSelection,\n context?: ExecutionContext,\n options?: { previewDrafts?: boolean },\n ): Promise<AnalyticsResult> {\n const compiled = this.registerDataset(dataset);\n this.logger.debug(`[Analytics] queryDataset \"${dataset.name}\" (object=${dataset.object}, include=${(dataset.include ?? []).join(',') || '—'})`);\n\n // ── ADR-0037 P3 — draft data preview ────────────────────────────────────\n // When the request renders the as-if-published world AND the base object\n // has a PENDING seed draft, evaluate the selection over the seed's rows in\n // memory (a query-evaluating proxy feeds the unchanged DatasetExecutor, so\n // measure filters / compareTo / derived measures all behave identically).\n // No pending seed → fall through to the real engine: published objects\n // keep charting live data even inside a preview.\n if (options?.previewDrafts && this.draftRowsResolver) {\n let seedRows: Record<string, unknown>[] | null = null;\n try {\n seedRows = await this.draftRowsResolver(dataset.object, context);\n } catch (e) {\n this.logger.warn(`[Analytics] draft preview resolver failed for \"${dataset.object}\" — falling back to live data: ${String((e as Error)?.message ?? e)}`);\n }\n if (seedRows) {\n this.logger.debug(`[Analytics] queryDataset \"${dataset.name}\" → preview over ${seedRows.length} drafted seed row(s)`);\n const previewService = {\n query: async (q: AnalyticsQuery) => evaluateAnalyticsQueryOverRows(q, compiled.cube, seedRows!),\n } as IAnalyticsService;\n const previewResult = await new DatasetExecutor(previewService).execute(compiled, selection, context);\n // Label resolution is skipped on purpose: drafted seed rows reference\n // lookups by NAME (the seed convention), which already reads well.\n return previewResult;\n }\n }\n\n // #3602 — every label lookup in this request (sort keys below, display\n // labels further down) reads the REFERENCED object, so bind that object's\n // own read scope to this request once, up front.\n const provider = this.readScopeProvider;\n const resolveScope = provider\n ? (targetObject: string) => provider(targetObject, context)\n : undefined;\n // #3680 — per-request label-fetch cache. A selection that sorts by a\n // lookup dimension resolves labels twice (pre-window sort keys, then\n // post-window display); the cache makes the display pass reuse the ids the\n // sort already fetched, so label-ordering costs ONE id→name read total.\n const labelDeps = this.labelResolver ? withLabelFetchCache(this.labelResolver) : undefined;\n // #3680 — hand the executor the sort-key label hook so an `order` on a\n // select/lookup dimension sorts by the label the user reads. Built over\n // the SAME capabilities (and read scope) as the display resolution below.\n const orderLabels = labelDeps && dataset.dimensions?.length\n ? createOrderLabelResolver(\n dataset.object,\n dataset.dimensions\n .filter((d) => !!d.field)\n .map((d) => ({ name: d.name, field: d.field as string })),\n labelDeps,\n resolveScope,\n context,\n )\n : undefined;\n\n // Graceful degradation: a dashboard/report widget whose backing object or\n // table is not present in this kernel (e.g. a platform dashboard like\n // System Overview that charts `sys_audit_log`, opened in an environment\n // that never mounted the audit object) must render as \"no data\" — NOT\n // crash the widget with a 500. Datasets were the one read surface that\n // hard-failed on a missing source.\n let result: AnalyticsResult;\n try {\n result = await new DatasetExecutor(this, orderLabels).execute(compiled, selection, context);\n } catch (err) {\n if (isMissingSourceError(err)) {\n this.logger.warn(\n `[Analytics] dataset \"${dataset.name}\" backing object \"${dataset.object}\" is unavailable ` +\n `(${String((err as Error)?.message ?? err)}); returning an empty result instead of failing the widget`,\n );\n return { rows: [], fields: [], totals: [] };\n }\n throw err;\n }\n\n // Selected dimensions resolved against the dataset definition — shared by\n // drill metadata, label resolution, and dimension field-label enrichment.\n const selectedDims = (selection.dimensions ?? [])\n .map((name) => dataset.dimensions?.find((d) => d.name === name))\n .filter((d): d is NonNullable<typeof d> => !!d);\n\n // ADR-0021 D2 — drill-through metadata. A host (dashboard/report) drills a\n // clicked bucket back to the underlying records, but it only knows the\n // dimension NAMES, and the label resolution below OVERWRITES the raw grouped\n // value in each row with its display label. So before that happens, snapshot\n // the raw grouped values into a PARALLEL array (aligned to `rows` by index —\n // the result rows are NOT mutated) and expose the dataset's `object` +\n // dimension→field mapping so the renderer can build an exact-match filter.\n // Date buckets are excluded — a humanized bucket (\"2026-06\") can't be\n // exact-matched against the stored timestamp, so they are not drillable.\n const drillDims = selectedDims.filter((d) => !!d.field && d.type !== 'date');\n if (drillDims.length && result.rows.length) {\n (result as AnalyticsResultWithDrill).object = dataset.object;\n (result as AnalyticsResultWithDrill).dimensionFields = Object.fromEntries(\n drillDims.map((d) => [d.name, d.field as string]),\n );\n (result as AnalyticsResultWithDrill).drillRawRows = result.rows.map((row) => {\n const raw: Record<string, unknown> = {};\n for (const d of drillDims) raw[d.name] = row[d.name];\n return raw;\n });\n // #3214 — the totals/subtotal rows (#1753) carry dimension values too and\n // go through the SAME label resolution below, so snapshot their raw\n // grouped values here in the same pre-label pass. Aligned to `result.totals`\n // by index; each grouping is restricted to the drillable dims it actually\n // groups by (the grand-total grouping `[]` keeps empty maps, so a subtotal\n // drill filters by the stored value while the grand total drills unfiltered).\n if (result.totals?.length) {\n (result as AnalyticsResultWithDrill).drillRawTotals = result.totals.map((total) => {\n const groupingDims = drillDims.filter((d) => total.dimensions.includes(d.name));\n return total.rows.map((row) => {\n const raw: Record<string, unknown> = {};\n for (const d of groupingDims) raw[d.name] = row[d.name];\n return raw;\n });\n });\n }\n }\n\n // #1752 — date-range drill scope. A `dateGranularity` dimension groups a\n // SPAN of records into one bucket, so drilling it needs a half-open range\n // `[gte, lt)`, which the equality `drillRawRows` sidecar can't express\n // (that's exactly why date dims are excluded from `drillDims` above). Emit\n // a parallel range sidecar computed — via the shared inverse util so server\n // and client agree on boundaries — from the canonical bucket KEY, which is\n // still in `rows[i][dim]` here (this runs BEFORE label resolution rewrites\n // it to a display label).\n const rangeTz = selection.timezone ?? context?.timezone ?? 'UTC';\n // Per drillable date+granularity dim, decide how to serialize its bounds\n // (ADR-0053 temporal semantics):\n // - `datetime` → the reference tz's MIDNIGHT INSTANT (ISO), because the\n // bucket is defined on that tz's calendar (works under any tz, incl. DST);\n // - `date` → `YYYY-MM-DD` calendar bounds, a tz-naive calendar day that is\n // exact under ANY reference tz;\n // - unknown field type → safe only under UTC (where the calendar day and\n // its instant coincide); under a non-UTC tz we can't tell whether to\n // shift, so the dim is omitted and the host drills a superset.\n // The bucket size to invert MUST be the one the query actually grouped by —\n // `selection.dateGranularity` overrides the dataset dimension's default\n // (#3588). Reading `d.dateGranularity` here meant a widget that bucketed by\n // quarter or year got its ranges computed from the dataset's month (or,\n // when the dataset declared none, dropped entirely) — so drilling a bucket\n // opened the wrong span, or the chart lost drill-through altogether.\n const rangeDims: Array<{ d: (typeof selectedDims)[number]; granularity: DateGranularityValue; instant: boolean }> = [];\n for (const d of selectedDims) {\n if (!d.field || d.type !== 'date') continue;\n const granularity = resolveDimensionGranularity(selection, d.name, d.dateGranularity);\n if (!granularity) continue;\n const ftype = this.measureCurrency?.(dataset.object, d.field as string)?.type;\n if (ftype === 'datetime') rangeDims.push({ d, granularity, instant: true });\n else if (ftype === 'date') rangeDims.push({ d, granularity, instant: false });\n else if (rangeTz === 'UTC') rangeDims.push({ d, granularity, instant: false });\n // else: unknown field type under a non-UTC reference tz → omit (superset).\n }\n if (rangeDims.length && result.rows.length) {\n const bound = (ymd: string, instant: boolean): string =>\n instant ? new Date(zonedDateStartToUtcMs(ymd, rangeTz)).toISOString() : ymd;\n (result as AnalyticsResultWithDrill).drillRanges = result.rows.map((row) => {\n const ranges: Record<string, { field: string; gte: string; lt: string }> = {};\n for (const { d, granularity, instant } of rangeDims) {\n // A row in the empty bucket carries `null` here (#3839) and yields no\n // range, so that row simply gets no drill bound — the superset.\n const cal = bucketKeyToCalendarRange(row[d.name] as string | null, granularity);\n if (cal) {\n ranges[d.name] = { field: d.field as string, gte: bound(cal.start, instant), lt: bound(cal.end, instant) };\n }\n }\n return ranges;\n });\n // The equality drill block sets `object` only when a NON-date drill dim\n // exists; a report grouped ONLY by time still needs the base object so the\n // host can open its list. Safe to (re)set to the same dataset object.\n (result as AnalyticsResultWithDrill).object = dataset.object;\n }\n\n // ADR-0021 — resolve grouped dimension values to human display labels\n // (select option label, lookup related-record name). Charts render the\n // dimension key verbatim, so this is the single place that turns a stored\n // value / FK id into the text a user expects to read.\n if (labelDeps && selectedDims.length) {\n // Same single-source rule as the drill ranges above: a date bucket must be\n // LABELLED with the granularity it was actually grouped by (#3588).\n // Formatting a `year` bucket with the dataset's `month` default rendered\n // it as \"1970-01\" — the year key re-parsed as an epoch millisecond count.\n const dims = selectedDims\n .filter((d) => !!d.field)\n .map((d) => ({\n name: d.name,\n field: d.field,\n type: d.type,\n dateGranularity: resolveDimensionGranularity(selection, d.name, d.dateGranularity),\n }));\n if (dims.length) {\n // `resolveScope` (hoisted above) binds the referenced object's read\n // scope to THIS request so the label lookup (a per-record read of the\n // related object) cannot surface a record the referenced object's RLS\n // would hide (#3602). `labelDeps` is the per-request cache over the\n // configured resolver, so ids the sort-key pass (#3680) already fetched\n // are not fetched again here.\n try {\n // `context` rides alongside `resolveScope` — the label lookup's SECOND\n // belt. `resolveScope` is this layer's own predicate; the context lets\n // the engine's middleware scope the same per-record read itself.\n await resolveDimensionLabels(dataset.object, dims, result.rows, labelDeps, resolveScope, context);\n // Totals rows (#1753) carry dimension values too (a row subtotal is\n // keyed by its row bucket) — resolve each grouping's own subset.\n for (const total of result.totals ?? []) {\n const subset = dims.filter((d) => total.dimensions.includes(d.name));\n if (subset.length) {\n await resolveDimensionLabels(dataset.object, subset, total.rows, labelDeps, resolveScope, context);\n }\n }\n } catch (e) {\n this.logger?.warn?.(`[Analytics] dimension label resolution failed for \"${dataset.name}\": ${String((e as Error)?.message ?? e)}`);\n }\n }\n }\n\n // ADR-0021 — enrich measure columns with their display `label` + `format`\n // so presentations show \"Tasks\" / \"$616,000\" instead of the raw measure\n // name \"task_count\" / \"616000\". Carried on the result fields; the renderer\n // applies the format (it can't be baked into the numeric row value).\n if (result.fields?.length && dataset.measures?.length) {\n const measureByName = new Map(dataset.measures.map((m) => [m.name, m]));\n for (const f of result.fields) {\n const m = measureByName.get(f.name) ?? measureByName.get(f.name.replace(/__compare$/, ''));\n if (!m) continue;\n if (f.label == null && typeof m.label === 'string') f.label = m.label;\n if (f.format == null && m.format) f.format = m.format;\n // ADR-0053 currency chain. A MONETARY measure resolves its display\n // currency from: explicit measure `currency` → source-field\n // `currencyConfig.defaultCurrency` → tenant default (`ctx.currency`). A\n // measure is monetary if it declares a currency OR aggregates a\n // `currency`-type field; non-monetary measures (count, avg of a plain\n // number) never receive a currency code.\n const fc = f as { currency?: string };\n const mc = m as { currency?: string };\n if (fc.currency == null) {\n const meta = m.field ? this.measureCurrency?.(dataset.object, m.field) : undefined;\n const monetary = !!mc.currency || meta?.type === 'currency';\n if (monetary) {\n const resolved = mc.currency ?? meta?.defaultCurrency ?? context?.currency;\n if (resolved) fc.currency = resolved;\n }\n }\n }\n }\n\n // Enrich DIMENSION columns with their display `label` too, so a grouped\n // table header reads \"Status\" instead of the raw field name \"status\". The\n // measure-only enrichment above left dimension headers bare (the renderer\n // then fell back to the raw dimension name).\n if (result.fields?.length && selectedDims.length) {\n const dimByName = new Map(selectedDims.map((d) => [d.name, d]));\n const dimByField = new Map(selectedDims.filter((d) => !!d.field).map((d) => [d.field as string, d]));\n for (const f of result.fields) {\n if (f.label != null) continue;\n // Result fields may be keyed by the dataset dimension NAME or the\n // underlying cube FIELD depending on strategy — match either.\n const d = dimByName.get(f.name) ?? dimByField.get(f.name);\n if (d && typeof d.label === 'string') f.label = d.label;\n }\n }\n return result;\n }\n\n /**\n * Get cube metadata for discovery.\n */\n async getMeta(cubeName?: string): Promise<CubeMeta[]> {\n // If a fallback service is configured, merge its metadata with the registry\n const cubes = cubeName\n ? [this.cubeRegistry.get(cubeName)].filter(Boolean) as Cube[]\n : this.cubeRegistry.getAll();\n\n return cubes.map(cube => ({\n name: cube.name,\n title: cube.title,\n measures: Object.entries(cube.measures).map(([key, measure]) => ({\n name: `${cube.name}.${key}`,\n type: measure.type,\n title: measure.label,\n })),\n dimensions: Object.entries(cube.dimensions).map(([key, dimension]) => ({\n name: `${cube.name}.${key}`,\n type: dimension.type,\n title: dimension.label,\n })),\n }));\n }\n\n /**\n * Generate SQL for a query without executing it (dry-run).\n */\n async generateSql(query: AnalyticsQuery, context?: ExecutionContext): Promise<{ sql: string; params: unknown[] }> {\n if (!query.cube) {\n throw new Error('Cube name is required for SQL generation');\n }\n\n this.ensureCube(query);\n const ctx = await this.callCtx(query, context);\n const strategy = this.resolveStrategy(query, ctx);\n this.logger.debug(`[Analytics] generateSql on cube \"${query.cube}\" → ${strategy.name}`);\n\n return strategy.generateSql(query, ctx);\n }\n\n // ── Internal ─────────────────────────────────────────────────────\n\n /**\n * Ensure a cube exists for the given query and that it knows about every\n * measure referenced by the query.\n *\n * - If no cube is registered for `query.cube`, infer a minimal cube from\n * the query so downstream strategies (which assume `cube.sql` exists)\n * don't crash.\n * - If a cube exists but the query references measures that aren't in\n * `cube.measures` (e.g. `amount_sum`, `amount_avg` emitted by dashboard\n * widget translators), inject suffix-inferred Metric entries so the\n * strategies pick the right aggregation function and field.\n */\n private ensureCube(query: AnalyticsQuery): void {\n const name = query.cube!;\n let cube = this.cubeRegistry.get(name);\n\n if (!cube) {\n // [#3867] Auto-inference below sets `cube.sql = name`, so from here on\n // the queried string IS a physical table name. Verify it names a\n // registered object BEFORE that happens — otherwise `/analytics/query`\n // is a way to aggregate over any table the connection can see, exactly\n // the hole #3770 closed on the data path. A registered Cube needs no\n // such check: it was authored, and its `sql` is whatever it declares.\n this.assertInferableCube(name);\n cube = this.inferCubeFromQuery(query);\n this.cubeRegistry.register(cube);\n // A scalar query — only measures, no grouping (no `dimensions`/\n // `timeDimensions`) — is the first-class \"metric over an object\" path\n // (e.g. the `object-metric` KPI widget). Auto-inferring a count/sum cube\n // is the intended behaviour there, so log at debug. A query that groups\n // by an explicit dimension or time bucket almost certainly meant to hit a\n // registered cube; keep that at warn so a forgotten registration is loud.\n const isScalarMetric =\n (query.dimensions?.length ?? 0) === 0 && (query.timeDimensions?.length ?? 0) === 0;\n const message =\n `[Analytics] No cube registered for \"${name}\"; auto-inferred a minimal cube ` +\n `(sql=\"${name}\", measures=${Object.keys(cube.measures).join(',') || '(none)'}, ` +\n `dimensions=${Object.keys(cube.dimensions).join(',') || '(none)'}). ` +\n `Define an explicit Cube in your stack for full control.`;\n if (isScalarMetric) this.logger.debug(message);\n else this.logger.warn(message);\n return;\n }\n\n // Cube exists — check for unknown measures referenced by the query and\n // augment the cube with suffix-inferred Metric definitions so callers\n // that pass `<field>_sum` / `<field>_avg` etc. get the right aggregation.\n const stripPrefix = (m: string) => (m.includes('.') ? m.split('.').slice(1).join('.') : m);\n const extraMeasures: Record<string, any> = {};\n for (const m of query.measures || []) {\n const key = stripPrefix(m);\n if (cube.measures[key] || extraMeasures[key]) continue;\n extraMeasures[key] = inferMeasure(key);\n }\n if (Object.keys(extraMeasures).length > 0) {\n const augmented: Cube = {\n ...cube,\n measures: { ...cube.measures, ...extraMeasures },\n };\n this.cubeRegistry.register(augmented);\n this.logger.debug(\n `[Analytics] Augmented cube \"${name}\" with inferred measures: ${Object.keys(extraMeasures).join(',')}`,\n );\n }\n }\n\n /**\n * [#3867] Gate on the cube auto-inference path: a name with no registered\n * Cube may only be inferred into one if it is a registered object.\n *\n * Rejects with `status: 404` / `code: 'CUBE_NOT_FOUND'` so the HTTP boundary\n * answers \"no such cube\" instead of letting the name reach the driver as a\n * table and surfacing whatever the driver says about it. The message names\n * both ways the request could be made valid, because from here the two are\n * genuinely indistinguishable: register a Cube, or register the object.\n *\n * Skips when `isRegisteredObject` was not supplied — see the config field's\n * doc for why that tier is a deliberate stand-down and not a hole.\n */\n private assertInferableCube(name: string): void {\n const isRegisteredObject = this.isRegisteredObject;\n if (!isRegisteredObject) {\n if (!this.warnedNoObjectRegistry) {\n this.warnedNoObjectRegistry = true;\n this.logger.warn(\n '[Analytics] no object-registry hook configured — the cube-inference existence gate ' +\n '(#3867) is INACTIVE for this service; an unregistered cube name reaches the driver ' +\n 'as a raw table name.',\n );\n }\n return;\n }\n if (isRegisteredObject(name)) return;\n const err = new Error(\n `Cube '${name}' not found: no cube is registered under that name, and it is not a ` +\n `registered object either (a cube can only be auto-inferred from a registered object). ` +\n `Define a Cube in your stack, or check the object name.`,\n ) as Error & { code?: string; status?: number; cube?: string };\n err.code = 'CUBE_NOT_FOUND';\n err.status = 404;\n err.cube = name;\n throw err;\n }\n\n /** Build a minimal Cube from the fields referenced by an AnalyticsQuery. */\n private inferCubeFromQuery(query: AnalyticsQuery): Cube {\n const cubeName = query.cube!;\n const measures: Record<string, any> = {};\n const dimensions: Record<string, any> = {};\n\n const stripPrefix = (m: string) => (m.includes('.') ? m.split('.').slice(1).join('.') : m);\n\n // Always provide a default `count` measure\n measures.count = { name: 'count', label: 'Count', type: 'count', sql: '*' };\n\n for (const m of query.measures || []) {\n const key = stripPrefix(m);\n if (measures[key]) continue;\n const inferred = inferMeasure(key);\n measures[key] = inferred;\n }\n\n for (const d of query.dimensions || []) {\n const key = stripPrefix(d);\n if (dimensions[key]) continue;\n dimensions[key] = { name: key, label: key, type: 'string', sql: key };\n }\n\n if (query.where && typeof query.where === 'object' && !Array.isArray(query.where)) {\n // Canonical FilterCondition: top-level keys (excluding logical\n // combinators) are field names. We only need them to seed an\n // ad-hoc cube definition for free-form queries.\n for (const key of Object.keys(query.where as Record<string, unknown>)) {\n if (key.startsWith('$')) continue;\n const stripped = stripPrefix(key);\n if (dimensions[stripped] || measures[stripped]) continue;\n dimensions[stripped] = { name: stripped, label: stripped, type: 'string', sql: stripped };\n }\n }\n\n for (const td of query.timeDimensions || []) {\n const key = stripPrefix(td.dimension);\n if (dimensions[key]) continue;\n dimensions[key] = {\n name: key, label: key, type: 'time', sql: key,\n granularities: ['day', 'week', 'month', 'quarter', 'year'],\n };\n }\n\n return {\n name: cubeName,\n title: cubeName,\n sql: cubeName,\n measures,\n dimensions,\n public: false,\n };\n }\n\n /**\n * Walk the strategy chain and return the first strategy that can handle the\n * query. `skip` excludes strategies that already proved incapable at\n * execution time (see {@link query}'s RAW_SQL_UNSUPPORTED fallback).\n */\n private resolveStrategy(\n query: AnalyticsQuery,\n ctx: StrategyContext,\n skip?: Set<AnalyticsStrategy>,\n ): AnalyticsStrategy {\n for (const strategy of this.strategies) {\n if (skip?.has(strategy)) continue;\n if (strategy.canHandle(query, ctx)) {\n return strategy;\n }\n }\n throw new Error(\n `[Analytics] No strategy can handle query for cube \"${query.cube}\". ` +\n `Checked: ${this.strategies.map(s => s.name).join(', ')}${skip?.size ? ` (skipped at runtime: ${[...skip].map((s) => s.name).join(', ')})` : ''}. ` +\n 'Ensure a compatible driver is configured or a fallback service is registered.',\n );\n }\n}\n\n/**\n * Infer a Metric definition from a measure key name.\n *\n * Recognised suffix conventions (matches dashboard widget translators that\n * emit measures like `<field>_sum`, `<field>_avg`):\n *\n * | Suffix | Aggregation |\n * |:-------------------|:----------------|\n * | `count` | `count(*)` |\n * | `_sum` | `sum(field)` |\n * | `_avg` / `_average`| `avg(field)` |\n * | `_min` | `min(field)` |\n * | `_max` | `max(field)` |\n * | `_count_distinct` | `count(distinct field)` |\n *\n * Anything else is treated as a `sum(<key>)` — best-effort default for an\n * unknown numeric measure.\n */\nexport function inferMeasure(key: string): { name: string; label: string; type: 'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'; sql: string } {\n if (key === 'count') {\n return { name: 'count', label: 'Count', type: 'count', sql: '*' };\n }\n const suffixes: Array<[string, 'sum' | 'avg' | 'min' | 'max' | 'count_distinct']> = [\n ['_count_distinct', 'count_distinct'],\n ['_sum', 'sum'],\n ['_avg', 'avg'],\n ['_average', 'avg'],\n ['_min', 'min'],\n ['_max', 'max'],\n ];\n for (const [suffix, type] of suffixes) {\n if (key.endsWith(suffix)) {\n const field = key.slice(0, -suffix.length) || '*';\n return { name: key, label: key, type, sql: field };\n }\n }\n return { name: key, label: key, type: 'sum', sql: key };\n}\n\n/**\n * FallbackDelegateStrategy — Internal strategy for fallback service delegation.\n *\n * Automatically added to the strategy chain when `fallbackService` is configured.\n * Not exported — consumers who need explicit in-memory support should use\n * `InMemoryStrategy` from `@objectstack/driver-memory`.\n */\nclass FallbackDelegateStrategy implements AnalyticsStrategy {\n readonly name = 'FallbackDelegateStrategy';\n readonly priority = 30;\n\n canHandle(query: AnalyticsQuery, ctx: StrategyContext): boolean {\n if (!query.cube) return false;\n return !!ctx.fallbackService;\n }\n\n async execute(query: AnalyticsQuery, ctx: StrategyContext): Promise<AnalyticsResult> {\n return ctx.fallbackService!.query(query);\n }\n\n async generateSql(query: AnalyticsQuery, ctx: StrategyContext): Promise<{ sql: string; params: unknown[] }> {\n if (ctx.fallbackService?.generateSql) {\n return ctx.fallbackService.generateSql(query);\n }\n return {\n sql: `-- FallbackDelegateStrategy: SQL generation not supported for cube \"${query.cube}\"`,\n params: [],\n };\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Cube } from '@objectstack/spec/data';\n\n/**\n * CubeRegistry — Central registry for analytics cube definitions.\n *\n * Cubes can be registered from two sources:\n * 1. **Manifest definitions** — Explicit cube definitions in `objectstack.config.ts`.\n * 2. **Object schema inference** — Auto-generated cubes from ObjectQL object schemas.\n *\n * The registry is the single source of truth for cube metadata discovery\n * (used by `getMeta()` and the strategy chain).\n */\nexport class CubeRegistry {\n private cubes = new Map<string, Cube>();\n\n /** Register a single cube definition. Overwrites if name already exists. */\n register(cube: Cube): void {\n this.cubes.set(cube.name, cube);\n }\n\n /** Register multiple cube definitions at once. */\n registerAll(cubes: Cube[]): void {\n for (const cube of cubes) {\n this.register(cube);\n }\n }\n\n /** Get a cube definition by name. */\n get(name: string): Cube | undefined {\n return this.cubes.get(name);\n }\n\n /** Check if a cube is registered. */\n has(name: string): boolean {\n return this.cubes.has(name);\n }\n\n /** Return all registered cubes. */\n getAll(): Cube[] {\n return Array.from(this.cubes.values());\n }\n\n /** Return all cube names. */\n names(): string[] {\n return Array.from(this.cubes.keys());\n }\n\n /** Number of registered cubes. */\n get size(): number {\n return this.cubes.size;\n }\n\n /** Remove all cubes. */\n clear(): void {\n this.cubes.clear();\n }\n\n /**\n * Auto-generate a cube definition from an object schema.\n *\n * Heuristic rules:\n * - `number` fields → `sum`, `avg`, `min`, `max` measures\n * - `boolean` fields → `count` measure (count where true)\n * - All non-computed fields → dimensions\n * - `date`/`datetime` fields → time dimensions with standard granularities\n * - A default `count` measure is always added\n *\n * @param objectName - The snake_case object name (used as table/cube name)\n * @param fields - Array of field descriptors `{ name, type, label? }`\n */\n inferFromObject(\n objectName: string,\n fields: Array<{ name: string; type: string; label?: string }>,\n ): Cube {\n const measures: Record<string, any> = {\n count: {\n name: 'count',\n label: 'Count',\n type: 'count',\n sql: '*',\n },\n };\n const dimensions: Record<string, any> = {};\n\n for (const field of fields) {\n const label = field.label || field.name;\n\n // All fields become dimensions\n const dimType = this.fieldTypeToDimensionType(field.type);\n dimensions[field.name] = {\n name: field.name,\n label,\n type: dimType,\n sql: field.name,\n ...(dimType === 'time'\n ? { granularities: ['day', 'week', 'month', 'quarter', 'year'] }\n : {}),\n };\n\n // Numeric fields also become aggregation measures\n if (field.type === 'number' || field.type === 'currency' || field.type === 'percent') {\n measures[`${field.name}_sum`] = {\n name: `${field.name}_sum`,\n label: `${label} (Sum)`,\n type: 'sum',\n sql: field.name,\n };\n measures[`${field.name}_avg`] = {\n name: `${field.name}_avg`,\n label: `${label} (Avg)`,\n type: 'avg',\n sql: field.name,\n };\n }\n }\n\n const cube: Cube = {\n name: objectName,\n title: objectName,\n sql: objectName,\n measures,\n dimensions,\n public: false,\n };\n\n this.register(cube);\n return cube;\n }\n\n private fieldTypeToDimensionType(fieldType: string): string {\n switch (fieldType) {\n case 'number':\n case 'currency':\n case 'percent':\n return 'number';\n case 'boolean':\n return 'boolean';\n case 'date':\n case 'datetime':\n return 'time';\n default:\n return 'string';\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Filter Normalization for the Analytics Layer\n *\n * The analytics endpoint accepts filters via the canonical `where`\n * field per the unified Query DSL (`spec/data/query.zod.ts`):\n *\n * - MongoDB-style FilterCondition: `{ field: value }` /\n * `{ field: { $op: value } }` / `{ $and: [...] }` — defined in\n * `spec/data/filter.zod.ts` and used by `find()`, dashboard\n * widget `filter`, RLS, etc.\n *\n * `normalizeAnalyticsFilters` flattens the FilterCondition tree into\n * the internal array form used by the SQL/Mongo pipeline strategies.\n * Strategies stay simple — they only need to know one shape — and the\n * spec is honoured: dashboard metadata is authored once in the\n * canonical MongoDB form and the server normalizes at the boundary.\n */\n\nexport interface NormalizedAnalyticsFilter {\n member: string;\n operator: string;\n values: string[];\n}\n\nconst MONGO_TO_CUBE_OP: Record<string, string> = {\n $eq: 'equals',\n $ne: 'notEquals',\n $gt: 'gt',\n $gte: 'gte',\n $lt: 'lt',\n $lte: 'lte',\n $in: 'in',\n $nin: 'notIn',\n $contains: 'contains',\n $notContains: 'notContains',\n $exists: 'set',\n};\n\n/**\n * Stringify a filter value as the internal pipeline requires `values: string[]`.\n *\n * Booleans serialize as the tokens `'true'`/`'false'` (NOT `'1'`/`'0'`) so the\n * boolean identity survives the string roundtrip: the consuming strategies can\n * recover a real boolean for the ObjectQL engine (which compares against the\n * stored boolean type) while still binding `1`/`0` for SQL. Stringifying to\n * `'1'`/`'0'` was indistinguishable from a numeric 1/0 and made every boolean\n * equality filter / boolean group-by compare a number against a boolean — and\n * never match.\n */\nfunction stringifyForCube(v: unknown): string {\n if (v == null) return '';\n if (typeof v === 'boolean') return v ? 'true' : 'false';\n if (v instanceof Date) return v.toISOString();\n if (typeof v === 'object') return JSON.stringify(v);\n return String(v);\n}\n\nfunction flattenCondition(cond: Record<string, unknown>, out: NormalizedAnalyticsFilter[]): void {\n for (const [key, raw] of Object.entries(cond)) {\n if (raw === undefined) continue;\n\n if (key === '$and' && Array.isArray(raw)) {\n for (const sub of raw) {\n if (sub && typeof sub === 'object') {\n flattenCondition(sub as Record<string, unknown>, out);\n }\n }\n continue;\n }\n // Logical $or / $not require recursive WHERE building which the\n // current strategies don't yet support; ignore so partial queries\n // still run.\n if (key === '$or' || key === '$not') continue;\n\n if (raw === null) {\n out.push({ member: key, operator: 'notSet', values: [] });\n continue;\n }\n\n if (typeof raw === 'object' && !Array.isArray(raw) && !(raw instanceof Date)) {\n const wrapper = raw as Record<string, unknown>;\n const opKeys = Object.keys(wrapper).filter(k => k.startsWith('$'));\n if (opKeys.length > 0) {\n for (const opKey of opKeys) {\n const cubeOp = MONGO_TO_CUBE_OP[opKey];\n if (!cubeOp) continue;\n const v = wrapper[opKey];\n const values = Array.isArray(v)\n ? v.map(stringifyForCube)\n : [stringifyForCube(v)];\n out.push({ member: key, operator: cubeOp, values });\n }\n continue;\n }\n // Nested relation (e.g. {profile: {verified: true}}). Flatten with\n // dot-prefixed keys so cube field path resolution still works.\n for (const [nestedKey, nestedVal] of Object.entries(wrapper)) {\n flattenCondition({ [`${key}.${nestedKey}`]: nestedVal }, out);\n }\n continue;\n }\n\n // Implicit equality / array → in\n if (Array.isArray(raw)) {\n out.push({ member: key, operator: 'in', values: raw.map(stringifyForCube) });\n } else {\n out.push({ member: key, operator: 'equals', values: [stringifyForCube(raw)] });\n }\n }\n}\n\n/**\n * Normalize an analytics query's `where` (FilterCondition) into the\n * internal array form used by all strategies.\n */\nexport function normalizeAnalyticsFilters(query: { where?: unknown } | unknown): NormalizedAnalyticsFilter[] {\n if (!query || typeof query !== 'object') return [];\n\n const out: NormalizedAnalyticsFilter[] = [];\n const where = (query as { where?: unknown }).where;\n\n if (where && typeof where === 'object' && !Array.isArray(where)) {\n flattenCondition(where as Record<string, unknown>, out);\n }\n\n return out;\n}\n\n/** Recover a finite number from a purely-numeric token, else undefined. */\nfunction recoverNumber(s: string): number | undefined {\n if (/^-?\\d+(\\.\\d+)?$/.test(s)) {\n const n = Number(s);\n if (Number.isFinite(n)) return n;\n }\n return undefined;\n}\n\n/**\n * Coerce a stringified filter value back into a runtime type for SQL\n * parameter binding. Better-sqlite3 (and most drivers) cannot bind a JS\n * boolean, so booleans are recovered as `1`/`0` integers; numbers are\n * recovered as numbers — avoiding string-vs-number mismatches against typed\n * columns.\n */\nexport function coerceFilterValueForSql(s: string): unknown {\n if (s === 'true') return 1;\n if (s === 'false') return 0;\n if (s === 'null') return null;\n return recoverNumber(s) ?? s;\n}\n\n/**\n * Coerce a stringified filter value back into a runtime type for the ObjectQL\n * aggregate engine. Unlike the SQL path, the engine compares against the\n * *stored* runtime type, so a boolean field holds a real `true`/`false` — bind\n * the boolean itself, NOT `1`/`0`, or the equality never matches.\n */\nexport function coerceFilterValueForObjectQL(s: string): unknown {\n if (s === 'true') return true;\n if (s === 'false') return false;\n if (s === 'null') return null;\n return recoverNumber(s) ?? s;\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { FilterCondition } from '@objectstack/spec/data';\n\n/**\n * Compile an RLS / tenant read-scope `FilterCondition` into a parameterized,\n * alias-qualified SQL predicate (ADR-0021 D-C).\n *\n * This is the single, security-critical translation point between the\n * canonical Mongo-style filter the `RLSCompiler` emits and the raw SQL the\n * analytics `NativeSQLStrategy` runs. It is deliberately:\n *\n * - **Fail-closed.** Any operator, value shape, or identifier it cannot\n * translate THROWS. A read-scope predicate must never be silently dropped —\n * dropping it would run the query unscoped and leak cross-tenant data.\n * - **Injection-safe.** Field/alias identifiers are validated against a strict\n * snake_case pattern and every value is bound as a `?` placeholder (the\n * strategy renumbers `?` → `$N`). No value is ever interpolated into SQL.\n * - **Alias-qualified.** Bare fields become `\"alias\".\"field\"` so the same\n * predicate applies to the base table or any joined table.\n *\n * Supports the operators the RLS layer and common policies emit: implicit\n * equality, `$eq/$ne/$gt/$gte/$lt/$lte/$in/$nin/$between/$contains/$notContains/\n * $startsWith/$endsWith/$null/$exists`, and `$and/$or/$not` combinators.\n */\n\nconst IDENT = /^[a-z_][a-z0-9_]*$/i;\n\nfunction quoteIdent(name: string, kind: string): string {\n if (typeof name !== 'string' || !IDENT.test(name)) {\n throw new Error(`[read-scope-sql] unsafe ${kind} identifier \"${String(name)}\" — refusing to build read scope (fail-closed).`);\n }\n return `\"${name}\"`;\n}\n\nexport function compileScopedFilterToSql(\n filter: FilterCondition,\n alias: string,\n): { sql: string; params: unknown[] } {\n const quotedAlias = quoteIdent(alias, 'alias');\n const params: unknown[] = [];\n const sql = compileNode(filter, quotedAlias, params);\n return { sql, params };\n}\n\n/** Compile a filter node into a boolean SQL expression ('' = empty/no constraint). */\nfunction compileNode(node: unknown, qAlias: string, params: unknown[]): string {\n if (node === null || typeof node !== 'object' || Array.isArray(node)) {\n throw new Error('[read-scope-sql] read scope must be a filter object (fail-closed).');\n }\n const clauses: string[] = [];\n for (const [key, value] of Object.entries(node as Record<string, unknown>)) {\n if (key === '$and' || key === '$or') {\n if (!Array.isArray(value) || value.length === 0) {\n throw new Error(`[read-scope-sql] \"${key}\" requires a non-empty array (fail-closed).`);\n }\n const parts = (value as unknown[])\n .map((child) => compileNode(child, qAlias, params))\n .filter((s) => s.length > 0);\n if (parts.length === 0) continue;\n const joiner = key === '$and' ? ' AND ' : ' OR ';\n clauses.push(`(${parts.join(joiner)})`);\n } else if (key === '$not') {\n const inner = compileNode(value, qAlias, params);\n if (inner) clauses.push(`NOT (${inner})`);\n } else if (key.startsWith('$')) {\n throw new Error(`[read-scope-sql] unsupported top-level operator \"${key}\" (fail-closed).`);\n } else {\n clauses.push(compileField(key, value, qAlias, params));\n }\n }\n return clauses.join(' AND ');\n}\n\n/** Compile a single `field: value | { $op: ... }` entry. */\nfunction compileField(field: string, value: unknown, qAlias: string, params: unknown[]): string {\n const col = `${qAlias}.${quoteIdent(field, 'field')}`;\n\n // Scalar / null → implicit equality.\n if (value === null) return `${col} IS NULL`;\n if (typeof value !== 'object' || value instanceof Date) {\n params.push(value);\n return `${col} = ?`;\n }\n if (Array.isArray(value)) {\n throw new Error(`[read-scope-sql] bare array value for \"${field}\" — use { $in: [...] } (fail-closed).`);\n }\n\n const ops = value as Record<string, unknown>;\n const keys = Object.keys(ops);\n // A value object must be ALL operators; a non-$ key means a nested relation,\n // which a flat read scope cannot join — fail closed.\n if (keys.length === 0 || keys.some((k) => !k.startsWith('$'))) {\n throw new Error(`[read-scope-sql] \"${field}\" has a nested/relation value which is not supported in a read scope (fail-closed).`);\n }\n\n const parts: string[] = [];\n for (const op of keys) {\n parts.push(compileOperator(col, op, ops[op], field, params));\n }\n return parts.length === 1 ? parts[0] : `(${parts.join(' AND ')})`;\n}\n\nfunction bind(params: unknown[], v: unknown): string {\n params.push(v);\n return '?';\n}\n\nfunction compileOperator(col: string, op: string, val: unknown, field: string, params: unknown[]): string {\n switch (op) {\n case '$eq': return val === null ? `${col} IS NULL` : `${col} = ${bind(params, val)}`;\n case '$ne': return val === null ? `${col} IS NOT NULL` : `${col} <> ${bind(params, val)}`;\n case '$gt': return `${col} > ${bind(params, val)}`;\n case '$gte': return `${col} >= ${bind(params, val)}`;\n case '$lt': return `${col} < ${bind(params, val)}`;\n case '$lte': return `${col} <= ${bind(params, val)}`;\n case '$in': {\n if (!Array.isArray(val)) throw new Error(`[read-scope-sql] $in for \"${field}\" needs an array (fail-closed).`);\n if (val.length === 0) return '1 = 0'; // IN () matches nothing — safe\n return `${col} IN (${val.map((v) => bind(params, v)).join(', ')})`;\n }\n case '$nin': {\n if (!Array.isArray(val)) throw new Error(`[read-scope-sql] $nin for \"${field}\" needs an array (fail-closed).`);\n if (val.length === 0) return '1 = 1'; // NOT IN () excludes nothing\n return `${col} NOT IN (${val.map((v) => bind(params, v)).join(', ')})`;\n }\n case '$between': {\n if (!Array.isArray(val) || val.length !== 2) throw new Error(`[read-scope-sql] $between for \"${field}\" needs [min,max] (fail-closed).`);\n return `${col} BETWEEN ${bind(params, val[0])} AND ${bind(params, val[1])}`;\n }\n case '$contains': return `${col} LIKE ${bind(params, `%${String(val)}%`)}`;\n case '$notContains': return `${col} NOT LIKE ${bind(params, `%${String(val)}%`)}`;\n case '$startsWith': return `${col} LIKE ${bind(params, `${String(val)}%`)}`;\n case '$endsWith': return `${col} LIKE ${bind(params, `%${String(val)}`)}`;\n case '$null': return val ? `${col} IS NULL` : `${col} IS NOT NULL`;\n case '$exists': return val ? `${col} IS NOT NULL` : `${col} IS NULL`;\n default:\n throw new Error(`[read-scope-sql] unsupported operator \"${op}\" on \"${field}\" (fail-closed).`);\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { AnalyticsQuery, AnalyticsResult } from '@objectstack/spec/contracts';\nimport type { Cube } from '@objectstack/spec/data';\nimport type { AnalyticsStrategy, StrategyContext } from './types.js';\nimport { normalizeAnalyticsFilters, coerceFilterValueForSql } from './filter-normalizer.js';\nimport { compileScopedFilterToSql } from '../read-scope-sql.js';\n\n/**\n * NativeSQLStrategy — Priority 1\n *\n * Pushes the analytics query down to the database as a native SQL statement.\n * This is the most efficient path and is preferred whenever the backing driver\n * supports raw SQL execution (e.g. Postgres, MySQL, SQLite).\n */\nexport class NativeSQLStrategy implements AnalyticsStrategy {\n readonly name = 'NativeSQLStrategy';\n readonly priority = 10;\n\n canHandle(query: AnalyticsQuery, ctx: StrategyContext): boolean {\n if (!query.cube) return false;\n // This strategy groups by the raw column expression (`GROUP BY <col>`) and\n // emits no `date_trunc` — it cannot bucket a date dimension to a coarser\n // granularity, nor resolve buckets on a non-UTC calendar. When the query\n // asks for granularity bucketing we therefore DECLINE so the lower-priority\n // ObjectQLStrategy handles it via `engine.aggregate` (native date_trunc when\n // UTC-safe, else uniform in-memory bucketing). Without this, a date-bucketed\n // query silently grouped by the raw timestamp — one bucket per row — and a\n // non-UTC reference timezone was ignored entirely (ADR-0053 Phase 2, #1982).\n if (query.timeDimensions?.some((td) => !!td.granularity)) return false;\n // ADR-0062 D6 — DECLINE federated (external-datasource) objects. This\n // strategy hand-compiles `FROM \"<object>\"` and bare column references, which\n // bypass the driver's physical-table resolution (`external.remoteName` /\n // `remoteSchema` / `columnMap`) and would query the WRONG table. Routing the\n // query to the lower-priority ObjectQL aggregate path keeps it correct —\n // that path goes through the driver's `getBuilder` (#2138/#2149). Applies to\n // the base object AND any joined object (a join would also hit the wrong\n // table). Until native-SQL learns the driver's resolution, \"disabled\" beats\n // \"silently wrong\".\n if (typeof ctx.isExternalObject === 'function') {\n const cube = ctx.getCube(query.cube);\n if (cube) {\n if (ctx.isExternalObject(this.extractObjectName(cube))) return false;\n const joinTargets = cube.joins ? Object.values(cube.joins) : [];\n for (const j of joinTargets) {\n const joinedObject = (j as { name?: string })?.name;\n if (joinedObject && ctx.isExternalObject(joinedObject)) return false;\n }\n }\n }\n const caps = ctx.queryCapabilities(query.cube);\n return caps.nativeSql && typeof ctx.executeRawSql === 'function';\n }\n\n async execute(query: AnalyticsQuery, ctx: StrategyContext): Promise<AnalyticsResult> {\n const { sql, params } = await this.generateSql(query, ctx);\n const cube = ctx.getCube(query.cube!)!;\n const objectName = this.extractObjectName(cube);\n\n const rows = await ctx.executeRawSql!(objectName, sql, params);\n\n // Build field metadata\n const fields = this.buildFieldMeta(query, cube);\n\n return { rows, fields, sql };\n }\n\n async generateSql(query: AnalyticsQuery, ctx: StrategyContext): Promise<{ sql: string; params: unknown[] }> {\n const cube = ctx.getCube(query.cube!);\n if (!cube) {\n throw new Error(`Cube not found: ${query.cube}`);\n }\n\n const params: unknown[] = [];\n const selectClauses: string[] = [];\n const groupByClauses: string[] = [];\n const tableName = this.extractObjectName(cube);\n // Map of relation alias → JOIN clause. Populated lazily as dotted\n // dimensions/measures/filters are resolved.\n const joins = new Map<string, string>();\n\n // Build SELECT for dimensions\n if (query.dimensions && query.dimensions.length > 0) {\n for (const dim of query.dimensions) {\n const colExpr = this.resolveDimensionSql(cube, dim, tableName, joins);\n selectClauses.push(`${colExpr} AS \"${dim}\"`);\n groupByClauses.push(colExpr);\n }\n }\n\n // Build SELECT for measures\n if (query.measures && query.measures.length > 0) {\n for (const measure of query.measures) {\n const aggExpr = this.resolveMeasureSql(cube, measure, tableName, joins);\n selectClauses.push(`${aggExpr} AS \"${measure}\"`);\n }\n }\n\n // Build WHERE clause\n const whereClauses: string[] = [];\n const normalizedFilters = normalizeAnalyticsFilters(query);\n if (normalizedFilters.length > 0) {\n for (const filter of normalizedFilters) {\n const colExpr = this.resolveFieldSql(cube, filter.member, tableName, joins);\n // Resolve the (object, column) this member binds against so the value\n // can be coerced to the column's storage form (see buildFilterClause).\n const target = this.resolveStorageTarget(cube, filter.member, tableName);\n const clause = this.buildFilterClause(colExpr, filter.operator, filter.values, params, ctx, target);\n if (clause) whereClauses.push(clause);\n }\n }\n\n // Build time dimension filters\n if (query.timeDimensions && query.timeDimensions.length > 0) {\n for (const td of query.timeDimensions) {\n const colExpr = this.resolveFieldSql(cube, td.dimension, tableName, joins);\n if (td.dateRange) {\n const range = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];\n if (range.length === 2) {\n // Same epoch-vs-text root cause as buildFilterClause: a dateRange on a\n // SQLite `Field.datetime` column compares ISO TEXT against an INTEGER\n // epoch and matches nothing. Coerce both bounds to the storage form.\n const td2 = this.resolveStorageTarget(cube, td.dimension, tableName);\n params.push(\n this.coerceTemporal(ctx, td2, range[0]),\n this.coerceTemporal(ctx, td2, range[1]),\n );\n whereClauses.push(`${colExpr} BETWEEN $${params.length - 1} AND $${params.length}`);\n }\n }\n }\n }\n\n // ── ADR-0021 D-C — enforce the join allowlist + inject per-object RLS ──\n // 1. Reject any join not backed by a relationship the dataset declared.\n const allowed = ctx.getAllowedRelationships?.(query.cube!);\n if (allowed) {\n for (const alias of joins.keys()) {\n if (!allowed.has(alias)) {\n throw new Error(\n `[NativeSQLStrategy] join \"${alias}\" is not backed by a declared relationship on ` +\n `cube \"${query.cube}\". v1 only joins along relationships listed in the dataset's \\`include\\`.`,\n );\n }\n }\n }\n // 2. Inject the tenant/RLS read scope for the base table AND every joined\n // object — this is the predicate the raw-SQL path would otherwise skip.\n this.applyReadScope(this.extractObjectName(cube), tableName, ctx, whereClauses, params);\n for (const alias of joins.keys()) {\n // The joined OBJECT (for the RLS lookup) is the target table from the\n // cube's join map; the ALIAS is how it's referenced in SQL. These differ\n // for namespaced objects (alias `account` → object `crm_account`).\n const joinedObject = cube.joins?.[alias]?.name ?? alias;\n this.applyReadScope(joinedObject, alias, ctx, whereClauses, params);\n }\n\n let sql = `SELECT ${selectClauses.join(', ')} FROM \"${tableName}\"`;\n if (joins.size > 0) {\n sql += ' ' + Array.from(joins.values()).join(' ');\n }\n if (whereClauses.length > 0) {\n sql += ` WHERE ${whereClauses.join(' AND ')}`;\n }\n if (groupByClauses.length > 0) {\n sql += ` GROUP BY ${groupByClauses.join(', ')}`;\n }\n if (query.order && Object.keys(query.order).length > 0) {\n const orderClauses = Object.entries(query.order).map(([f, d]) => `\"${f}\" ${d.toUpperCase()}`);\n sql += ` ORDER BY ${orderClauses.join(', ')}`;\n }\n if (query.limit != null) {\n sql += ` LIMIT ${query.limit}`;\n }\n if (query.offset != null) {\n sql += ` OFFSET ${query.offset}`;\n }\n\n return { sql, params };\n }\n\n // ── Helpers ──────────────────────────────────────────────────────\n\n /**\n * ADR-0021 D-C — inject an object's read scope (tenant + RLS predicate) into\n * the WHERE clause. The scope is a canonical `FilterCondition` (what the\n * RLSCompiler emits); `compileScopedFilterToSql` turns it into alias-qualified,\n * parameterized SQL (fail-closed — it throws rather than drop a predicate).\n * The `?` placeholders are then renumbered into the strategy's `$N` scheme.\n * No-op when the runtime provides no scope hook (the caller is then\n * responsible for isolation — see contract note).\n */\n private applyReadScope(\n objectName: string,\n alias: string,\n ctx: StrategyContext,\n whereClauses: string[],\n params: unknown[],\n ): void {\n if (typeof ctx.getReadScope !== 'function') return;\n const filter = ctx.getReadScope(objectName);\n if (filter === undefined || filter === null) return;\n const { sql, params: scopeParams } = compileScopedFilterToSql(filter, alias);\n if (!sql) return;\n let i = 0;\n const rendered = sql.replace(/\\?/g, () => {\n params.push(scopeParams[i++]);\n return `$${params.length}`;\n });\n whereClauses.push(`(${rendered})`);\n }\n\n /** SQL-safe join alias for a relationship path (dots → `__`); single-segment\n * paths are unchanged. Mirrors the dataset compiler's `cube.joins` keying so\n * alias, allowlist, and per-hop RLS all agree on one valid identifier. */\n private joinAlias(path: string): string {\n return path.replace(/\\./g, '__');\n }\n\n /**\n * Resolve a dimension/measure/filter SQL expression that may reference a\n * related table via dot notation (e.g. `account.industry`).\n *\n * A dotted `sql` is a relationship PATH (ADR-0071 multi-hop): every segment\n * but the last is a to-one relationship hop, the last is the column. Each hop\n * synthesises a `LEFT JOIN` aliased by its full path prefix, chained\n * parent→child. The convention (matching the auto-cube generator and\n * ObjectStack object schemas) for a single hop is:\n *\n * <parentTable>.<lookupField> = <lookupField>.id\n *\n * i.e. the lookup field name on the parent table equals the related\n * table name. This holds for all `Field.lookup({ object: '...' })`\n * declarations where the field is named after its target object.\n *\n * Returns the qualified SQL reference (e.g. `\"account\".\"industry\"`).\n * Pure column references (no dot) are returned as-is.\n */\n private qualifyAndRegisterJoin(\n rawSql: string,\n parentTable: string,\n joins: Map<string, string>,\n cube?: Cube,\n ): string {\n if (!rawSql.includes('.')) {\n // Base-table column. When the cube can join other tables, a bare column\n // that also exists on a joined table (e.g. base `status` vs joined\n // `account.status`) makes the SQL engine raise \"ambiguous column name\".\n // Qualify plain identifiers with the base table; leave SQL expressions\n // and `*` untouched. Single-object cubes (no joins) keep bare columns so\n // their generated SQL is byte-for-byte unchanged.\n const canJoin = !!cube?.joins && Object.keys(cube.joins).length > 0;\n if (canJoin && /^[A-Za-z_][A-Za-z0-9_]*$/.test(rawSql)) {\n return `\"${parentTable}\".\"${rawSql}\"`;\n }\n return rawSql;\n }\n // Multi-hop (ADR-0071): the dotted path IS the join chain. Every segment but\n // the last is a relationship hop; the last is the column. The join ALIAS at\n // each hop is the full path PREFIX (`account`, then `account.owner`), which\n // encodes its own parent (the prefix minus its last segment) and FK column\n // (that segment). Register one LEFT JOIN per prefix, chaining parent→child.\n const segments = rawSql.split('.');\n const column = segments[segments.length - 1];\n const hops = segments.slice(0, -1);\n if (hops.length === 0 || !column) return rawSql;\n let parentAlias = parentTable;\n let prefix = '';\n for (const seg of hops) {\n prefix = prefix ? `${prefix}.${seg}` : seg;\n const alias = this.joinAlias(prefix);\n if (!joins.has(alias)) {\n // The joined TABLE is resolved from the Cube's `joins` map (emitted by\n // the dataset compiler, keyed by the same alias); fall back to the alias\n // as the table for legacy/same-name cubes.\n const joinTable = cube?.joins?.[alias]?.name ?? alias;\n // Only emit an explicit alias when the table differs from it; when they\n // match, `LEFT JOIN \"account\" ON …` is cleaner (and back-compat).\n const tableRef = joinTable === alias ? `\"${alias}\"` : `\"${joinTable}\" \"${alias}\"`;\n joins.set(\n alias,\n `LEFT JOIN ${tableRef} ON \"${parentAlias}\".\"${seg}\" = \"${alias}\".\"id\"`,\n );\n }\n parentAlias = alias;\n }\n return `\"${parentAlias}\".\"${column}\"`;\n }\n\n /**\n * Resolve a member reference (dimension, measure, or filter field) to its\n * cube definition.\n *\n * Accepts three naming conventions:\n * 1. `<cube>.<field>` — the canonical analytics qualifier (stripped to `<field>`).\n * 2. `<lookup>.<field>` — a relation traversal (e.g. `account.industry`).\n * First tried as the literal key, then as the underscore-flattened\n * key (`account_industry`), and finally returned as a synthetic\n * definition whose `sql` is the dotted reference so the JOIN\n * machinery can pick it up.\n * 3. `<field>` — a bare field name on the cube's table.\n */\n private lookupMember(\n cube: Cube,\n member: string,\n kind: 'dimension' | 'measure',\n ): { sql: string; type?: string } | undefined {\n const bag = kind === 'dimension' ? cube.dimensions : cube.measures;\n // Direct hit on the registered key (handles `cube.field` and exact dotted keys).\n if (bag[member]) return bag[member];\n if (member.includes('.')) {\n const [first, ...rest] = member.split('.');\n const tail = rest.join('.');\n // `<cube>.<field>` style.\n if (first === cube.name && bag[tail]) return bag[tail];\n // Plain second-segment lookup (legacy behaviour).\n if (bag[tail]) return bag[tail];\n // Underscore-flattened relation lookup (e.g. `account_industry`).\n const flat = member.replace(/\\./g, '_');\n if (bag[flat]) return bag[flat];\n // Synthetic relation traversal — let qualifyAndRegisterJoin handle it.\n if (kind === 'dimension') {\n return { sql: member, type: 'string' };\n }\n } else if (bag[member]) {\n return bag[member];\n }\n return undefined;\n }\n\n private resolveDimensionSql(\n cube: Cube,\n member: string,\n parentTable: string,\n joins: Map<string, string>,\n ): string {\n const dim = this.lookupMember(cube, member, 'dimension');\n const raw = dim ? dim.sql : (member.includes('.') ? member.split('.')[1] : member);\n return this.qualifyAndRegisterJoin(raw, parentTable, joins, cube);\n }\n\n private resolveMeasureSql(\n cube: Cube,\n member: string,\n parentTable: string,\n joins: Map<string, string>,\n ): string {\n const measure = this.lookupMember(cube, member, 'measure') as\n | { sql: string; type: string }\n | undefined;\n if (!measure) return `COUNT(*)`;\n\n const col = measure.sql === '*'\n ? '*'\n : this.qualifyAndRegisterJoin(measure.sql, parentTable, joins, cube);\n switch (measure.type) {\n case 'count': return 'COUNT(*)';\n case 'sum': return `SUM(${col})`;\n case 'avg': return `AVG(${col})`;\n case 'min': return `MIN(${col})`;\n case 'max': return `MAX(${col})`;\n case 'count_distinct': return `COUNT(DISTINCT ${col})`;\n default: return `COUNT(*)`;\n }\n }\n\n private resolveFieldSql(\n cube: Cube,\n member: string,\n parentTable: string,\n joins: Map<string, string>,\n ): string {\n const dim = this.lookupMember(cube, member, 'dimension');\n if (dim) return this.qualifyAndRegisterJoin(dim.sql, parentTable, joins, cube);\n const measure = this.lookupMember(cube, member, 'measure');\n if (measure) return this.qualifyAndRegisterJoin(measure.sql, parentTable, joins, cube);\n const fieldName = member.includes('.') ? member.split('.')[1] : member;\n return fieldName;\n }\n\n /**\n * Resolve the (object, column) a filter member binds against, so its\n * comparand can be coerced to that column's on-disk storage form.\n *\n * Mirrors `resolveFieldSql`'s `sql` resolution but yields the *logical*\n * target rather than the qualified SQL:\n * - A dotted column (`account.region`, emitted for a relation traversal)\n * belongs to the JOINED object — resolve the alias → target table via the\n * cube's `joins` map (alias `account` → object `crm_account` when\n * namespaced) and take the tail as the column.\n * - Otherwise the column lives on the cube's BASE table. Use the dimension's\n * resolved `sql` (the real column, which may differ from the member name,\n * e.g. dimension `assessed` → column `assessed_at`) rather than the member.\n */\n private resolveStorageTarget(\n cube: Cube,\n member: string,\n baseTable: string,\n ): { object: string; field: string } {\n const dim = this.lookupMember(cube, member, 'dimension');\n const measure = dim ? undefined : this.lookupMember(cube, member, 'measure');\n const rawSql = dim?.sql ?? measure?.sql ?? (member.includes('.') ? member.split('.').slice(1).join('.') : member);\n\n if (rawSql.includes('.')) {\n // Multi-hop (ADR-0071): the column's owning object is the join at the\n // relationship PATH (all segments but the last); the column is the last.\n const segments = rawSql.split('.');\n const field = segments[segments.length - 1];\n const relPath = segments.slice(0, -1).join('.');\n const object = cube.joins?.[this.joinAlias(relPath)]?.name ?? relPath;\n return { object, field };\n }\n return { object: baseTable, field: rawSql };\n }\n\n /**\n * Apply the storage-form coercion for a single comparand. Prefers the\n * driver-backed `coerceTemporalFilterValue` hook (single source of truth for\n * the date/datetime storage convention — see StrategyContext); when the hook\n * is absent, or returns the value unchanged (the field is not a temporal\n * column, or the dialect stores it as a native timestamp), falls back to the\n * generic boolean/number recovery so non-temporal typed columns still bind\n * correctly.\n */\n private coerceTemporal(\n ctx: StrategyContext,\n target: { object: string; field: string },\n value: string,\n ): unknown {\n if (typeof ctx.coerceTemporalFilterValue === 'function') {\n const coerced = ctx.coerceTemporalFilterValue(target.object, target.field, value);\n // Hook returns the value untouched for non-temporal / native-timestamp\n // columns; only short-circuit when it actually changed the value.\n if (coerced !== value) return coerced;\n }\n return coerceFilterValueForSql(value);\n }\n\n private buildFilterClause(\n col: string,\n operator: string,\n values: string[] | undefined,\n params: unknown[],\n ctx: StrategyContext,\n target: { object: string; field: string },\n ): string | null {\n const opMap: Record<string, string> = {\n equals: '=', notEquals: '!=', gt: '>', gte: '>=', lt: '<', lte: '<=',\n contains: 'LIKE', notContains: 'NOT LIKE',\n };\n\n if (operator === 'set') return `${col} IS NOT NULL`;\n if (operator === 'notSet') return `${col} IS NULL`;\n\n if (operator === 'in' || operator === 'notIn') {\n if (!values || values.length === 0) return null;\n // Dates can legitimately appear in an `in`/`notIn` set (e.g. a multi-day\n // KPI), so coerce each element to the column's storage form too — same\n // SQLite epoch-vs-text root cause as the scalar operators below.\n const placeholders = values.map(v => { params.push(this.coerceTemporal(ctx, target, v)); return `$${params.length}`; }).join(', ');\n return `${col} ${operator === 'in' ? 'IN' : 'NOT IN'} (${placeholders})`;\n }\n\n const sqlOp = opMap[operator];\n if (!sqlOp || !values || values.length === 0) return null;\n\n if (operator === 'contains' || operator === 'notContains') {\n params.push(`%${values[0]}%`);\n } else {\n // Coerce so booleans/numbers bind as their native SQL types AND so a\n // relative-date / ISO-string comparand on a SQLite `Field.datetime`\n // column is converted to its INTEGER epoch storage form. Without this a\n // dashboard filter like `assessed_at >= '2025-06-18'` compiles to a\n // TEXT-vs-INTEGER affinity compare that is always false → \"No rows\",\n // even though the rows exist (the confirmed time-series chart bug).\n params.push(this.coerceTemporal(ctx, target, values[0]));\n }\n return `${col} ${sqlOp} $${params.length}`;\n }\n\n private extractObjectName(cube: Cube): string {\n return cube.sql.trim();\n }\n\n private buildFieldMeta(query: AnalyticsQuery, cube: Cube): Array<{ name: string; type: string }> {\n const fields: Array<{ name: string; type: string }> = [];\n if (query.dimensions) {\n for (const dim of query.dimensions) {\n const d = this.lookupMember(cube, dim, 'dimension');\n fields.push({ name: dim, type: d?.type || 'string' });\n }\n }\n if (query.measures) {\n for (const m of query.measures) {\n fields.push({ name: m, type: 'number' });\n }\n }\n return fields;\n }\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Cross-object dimension re-bucketing (#3654 capability).\n *\n * `engine.aggregate()` cannot join, so the ObjectQL path cannot group directly\n * by a related object's attribute (`account.region`). Instead the strategy:\n * 1. groups the base aggregate by the LOOKUP FK column (`account`), which the\n * engine CAN do (it is a plain base column), and\n * 2. resolves each FK id to the related attribute (`region`) with a SCOPED\n * read of the referenced object, then\n * 3. re-buckets the base aggregate by that attribute here, in memory,\n * recombining the measures.\n *\n * This module is the pure, deterministic step (3): given the base rows, the\n * FK→attribute maps, and the measures' aggregation methods, produce the rows a\n * direct cross-object grouping would have. It is unit-tested in isolation\n * because a wrong re-combination silently corrupts totals — exactly the class of\n * bug #3654 exists to kill.\n *\n * A base row whose FK does not resolve (the referenced record is hidden by the\n * referenced object's own RLS) buckets under {@link RESTRICTED_BUCKET}: its\n * measure still counts, so grand totals are preserved, but the hidden record's\n * attribute value never appears (no leak — ADR-0021 D-C, the #3602 class).\n */\n\n/** Only aggregation methods that re-combine across sub-buckets are supported. */\nexport type RecombinableMethod = 'sum' | 'count' | 'min' | 'max';\n\nexport const RECOMBINABLE_METHODS: ReadonlySet<string> = new Set<RecombinableMethod>([\n 'sum',\n 'count',\n 'min',\n 'max',\n]);\n\n/** Sentinel bucket for base rows whose referenced record the caller cannot read. */\nexport const RESTRICTED_BUCKET = '(restricted)';\n\nexport interface CrossObjectDim {\n /** Output key for the resolved attribute (the original dimension name), e.g. `region`. */\n outputName: string;\n /** The base FK column the base aggregate was grouped by, e.g. `account`. */\n fkField: string;\n /** `fkValue → attributeValue`. An FK absent from the map buckets as RESTRICTED. */\n fkToAttr: Map<unknown, unknown>;\n}\n\nexport interface MeasureRecombine {\n /** The measure's output key in each base row. */\n alias: string;\n method: RecombinableMethod;\n}\n\n/**\n * Order two measure values. A number orders as itself; a `Date` or an ISO\n * timestamp orders as its instant, so a `min`/`max` over a temporal measure\n * compares correctly instead of collapsing to `NaN` (#3797). `NaN` means \"not\n * orderable\" and the caller keeps the other side.\n */\nfunction orderableValue(v: unknown): number {\n if (v == null) return NaN;\n if (typeof v === 'number') return v;\n if (v instanceof Date) return v.getTime();\n const n = Number(v);\n if (Number.isFinite(n)) return n;\n return Date.parse(String(v));\n}\n\n/**\n * Combine two measure values under an aggregation method (either may be\n * undefined).\n *\n * `sum`/`count` are numeric by construction and stay so. `min`/`max` return the\n * winning ORIGINAL value rather than a number: the value they pick is a value\n * OF the column, so a temporal measure has to come back out in the same shape\n * the driver presented it (#3797) — coercing it to a number here would put the\n * epoch leak back one layer up, and on any dialect whose driver returns an ISO\n * string it would produce `NaN` outright.\n */\nfunction recombine(method: RecombinableMethod, acc: unknown, next: unknown): unknown {\n if (method === 'min' || method === 'max') {\n if (acc === undefined) return next ?? 0;\n const a = orderableValue(acc);\n const n = orderableValue(next);\n if (Number.isNaN(n)) return acc;\n if (Number.isNaN(a)) return next;\n const nextWins = method === 'min' ? n < a : n > a;\n return nextWins ? next : acc;\n }\n const n = Number(next ?? 0);\n return acc === undefined ? n : Number(acc) + n;\n}\n\n/**\n * Re-bucket base aggregate rows by resolved cross-object attributes.\n *\n * @param baseRows rows grouped by `baseDimFields` + every `crossDims[*].fkField`.\n * @param baseDimFields the NON-cross-object group keys carried through unchanged\n * (base columns and date buckets), keyed as in `baseRows`.\n * @param crossDims one entry per cross-object dimension (its FK→attr map).\n * @param measures measure keys + their (recombinable) aggregation method.\n * @returns rows keyed by `baseDimFields` + each `crossDims[*].outputName` + measures.\n */\nexport function rebucketCrossObject(\n baseRows: Record<string, unknown>[],\n baseDimFields: string[],\n crossDims: CrossObjectDim[],\n measures: MeasureRecombine[],\n): Record<string, unknown>[] {\n const buckets = new Map<string, Record<string, unknown>>();\n\n for (const row of baseRows) {\n // Resolve each cross-object FK to its attribute (or RESTRICTED).\n const resolved: Record<string, unknown> = {};\n for (const cd of crossDims) {\n const fk = row[cd.fkField];\n resolved[cd.outputName] = cd.fkToAttr.has(fk) ? cd.fkToAttr.get(fk) : RESTRICTED_BUCKET;\n }\n\n // Bucket key = base dims (unchanged) + resolved attributes. `\u0001` is a\n // separator no group value contains, matching the engine's own convention.\n const keyParts: string[] = [];\n // JSON-encoded, so the empty bucket (`null` on both aggregation paths since\n // #3839) stays distinct from a row whose value is the literal string\n // `\"null\"` — plain interpolation renders both as `null` and would merge two\n // real groups into one. Only this composite id is affected; the emitted\n // bucket keeps the row's own value verbatim below.\n for (const f of baseDimFields) keyParts.push(`${f}=${JSON.stringify(row[f] ?? null)}`);\n for (const cd of crossDims) keyParts.push(`${cd.outputName}=${String(resolved[cd.outputName])}`);\n const key = keyParts.join('\u0001');\n\n let bucket = buckets.get(key);\n if (!bucket) {\n bucket = {};\n for (const f of baseDimFields) bucket[f] = row[f];\n for (const cd of crossDims) bucket[cd.outputName] = resolved[cd.outputName];\n buckets.set(key, bucket);\n }\n for (const m of measures) {\n bucket[m.alias] = recombine(m.method, bucket[m.alias], row[m.alias]);\n }\n }\n\n return [...buckets.values()];\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { AnalyticsQuery, AnalyticsResult } from '@objectstack/spec/contracts';\nimport type { Cube } from '@objectstack/spec/data';\nimport type { AnalyticsStrategy, StrategyContext } from './types.js';\nimport { normalizeAnalyticsFilters, coerceFilterValueForObjectQL } from './filter-normalizer.js';\nimport { compileScopedFilterToSql } from '../read-scope-sql.js';\nimport {\n rebucketCrossObject,\n RECOMBINABLE_METHODS,\n type CrossObjectDim,\n type MeasureRecombine,\n type RecombinableMethod,\n} from './cross-object-rebucket.js';\n\n/** Scalar analytics operators → their SQL spelling (display SQL only). */\nconst SCALAR_SQL_OPS: Record<string, string> = {\n equals: '=', notEquals: '!=', gt: '>', gte: '>=', lt: '<', lte: '<=',\n};\n\n/** One cross-object grouping dimension planned for FK-expand (#3654). */\ninterface CrossObjectPlanDim {\n /** The caller's dimension name (output key), e.g. `region`. */\n outputName: string;\n /** The base lookup FK column to group the base aggregate by, e.g. `account`. */\n fkField: string;\n /** The related object's attribute to resolve the FK to, e.g. `region`. */\n attr: string;\n /** The related object name (join target), e.g. `crm_account`. */\n refObject: string;\n}\n\ninterface CrossObjectPlan {\n crossDims: CrossObjectPlanDim[];\n}\n\n/**\n * ObjectQLStrategy — Priority 2\n *\n * Translates an analytics query into an ObjectQL `engine.aggregate()` call.\n * This path works with any driver that supports the ObjectQL aggregate AST\n * (Postgres, Mongo, SQLite, etc.) without requiring raw SQL access.\n */\nexport class ObjectQLStrategy implements AnalyticsStrategy {\n readonly name = 'ObjectQLStrategy';\n readonly priority = 20;\n\n canHandle(query: AnalyticsQuery, ctx: StrategyContext): boolean {\n if (!query.cube) return false;\n const caps = ctx.queryCapabilities(query.cube);\n return caps.objectqlAggregate && typeof ctx.executeAggregate === 'function';\n }\n\n async execute(query: AnalyticsQuery, ctx: StrategyContext): Promise<AnalyticsResult> {\n const cube = ctx.getCube(query.cube!)!;\n const objectName = this.extractObjectName(cube);\n\n // Build groupBy from dimensions, honouring `timeDimensions` granularity.\n // A date dimension with a granularity becomes a STRUCTURED groupBy item\n // `{ field, dateGranularity }` — which `engine.aggregate()` buckets (driver\n // date_trunc or in-memory). Without this the ObjectQL path grouped raw\n // timestamps (one bucket per row) and date-bucketed dataset widgets never\n // matched their legacy `categoryGranularity` counterpart.\n type GroupByItem = string | { field: string; dateGranularity: string };\n const granByDim = new Map<string, string>();\n for (const td of query.timeDimensions ?? []) {\n if (td.granularity) granByDim.set(td.dimension, td.granularity);\n }\n const groupBy: GroupByItem[] = [];\n if (query.dimensions && query.dimensions.length > 0) {\n for (const dim of query.dimensions) {\n const field = this.resolveFieldName(cube, dim, 'dimension');\n const gran = granByDim.get(dim);\n groupBy.push(gran ? { field, dateGranularity: gran } : field);\n granByDim.delete(dim);\n }\n }\n // Time dimensions not also listed in `dimensions` still bucket + group.\n for (const [dim, gran] of granByDim) {\n groupBy.push({ field: this.resolveFieldName(cube, dim, 'dimension'), dateGranularity: gran });\n }\n\n // Build aggregations from measures\n const aggregations: Array<{ field: string; method: string; alias: string }> = [];\n if (query.measures && query.measures.length > 0) {\n for (const measure of query.measures) {\n const { field, method } = this.resolveMeasureAggregation(cube, measure);\n aggregations.push({ field, method, alias: measure });\n }\n }\n\n // Build the engine filter. Every predicate — the caller's `where` and the\n // time-dimension windows alike — is contributed through\n // `mergeFilterOperand`, because one field routinely carries MULTIPLE\n // operators (a range `{$gte, $lte}` on `close_date`) and a plain assignment\n // would keep only the last.\n const filter: Record<string, unknown> = {};\n // Operands that cannot merge into their field's entry without one silently\n // replacing the other; ANDed in below so the engine intersects them.\n const conjuncts: Record<string, unknown>[] = [];\n for (const f of normalizeAnalyticsFilters(query)) {\n const fieldName = this.resolveFieldName(cube, f.member, 'any');\n const extra = this.mergeFilterOperand(filter, fieldName, this.convertFilter(f.operator, f.values));\n if (extra) conjuncts.push(extra);\n }\n // #3650 — and the time-dimension WINDOWS, through the SAME merge, so a\n // `dateRange` and a caller `where` bound on one field compose instead of\n // clobbering each other.\n for (const { field, bounds } of this.dateRangeBounds(cube, query)) {\n const extra = this.mergeFilterOperand(filter, field, bounds);\n if (extra) conjuncts.push(extra);\n }\n if (conjuncts.length > 0) {\n filter.$and = [...(Array.isArray(filter.$and) ? filter.$and : []), ...conjuncts];\n }\n\n // #3654 — classify cross-object references. A cross-object DIMENSION within\n // the supported envelope is served by an FK-expand (`executeCrossObject`);\n // everything the engine cannot serve (cross-object measures/filters,\n // multi-hop, non-recombinable measures) is REJECTED by `planCrossObject` —\n // the engine has no join, and a silent mis-bucket is worse than a loud\n // error. `null` ⇒ the query is base-only and takes the direct path below.\n const plan = this.planCrossObject(cube, query, filter);\n if (plan) {\n return this.executeCrossObject(cube, query, aggregations, filter, plan, ctx);\n }\n\n // ADR-0021 D-C — the base object's read scope (tenant + RLS) MUST be ANDed\n // in before the query leaves the strategy (#3597). A base-only query has a\n // single object in play, so one base-object scope is sufficient here.\n const rows = await ctx.executeAggregate!(objectName, {\n // Structured groupBy items ({field, dateGranularity}) pass through the\n // executeAggregate bridge to engine.aggregate, which buckets them. The\n // contract types groupBy as string[]; the cast carries the richer shape.\n groupBy: groupBy.length > 0 ? (groupBy as unknown as string[]) : undefined,\n aggregations: aggregations.length > 0 ? aggregations : undefined,\n filter: this.withReadScope(objectName, filter, ctx),\n // ADR-0053 Phase 2 (D2): forward the reference tz so date buckets resolve\n // on that zone's calendar days. A non-UTC zone makes the engine bucket\n // in-memory (uniform across drivers); UTC/unset keeps the DB fast path.\n timezone: query.timezone,\n // ADR-0021 D-C (#3602): the second belt. `withReadScope` above is this\n // layer's own scoping; handing the engine the context makes ITS middleware\n // inject RLS too, so a future strategy that forgets `withReadScope` still\n // cannot read across tenants. Without it the operation reaches the engine\n // principal-less and plugin-security falls open — the #3597 shape.\n context: ctx.context,\n });\n\n // Remap short field names back to cube-qualified names\n const mappedRows = rows.map(row => {\n const mapped: Record<string, unknown> = {};\n if (query.dimensions) {\n for (const dim of query.dimensions) {\n const shortName = this.resolveFieldName(cube, dim, 'dimension');\n if (shortName in row) mapped[dim] = row[shortName];\n }\n }\n if (query.measures) {\n for (const m of query.measures) {\n // Alias was set to the full measure name\n if (m in row) mapped[m] = row[m];\n }\n }\n return mapped;\n });\n\n const fields = this.buildFieldMeta(query, cube);\n // Echo a representative SQL alongside the rows (#3588). `NativeSQLStrategy`\n // returns the statement it actually ran, and dataset responses surface that\n // string — it is how an author checks what their widget compiled to. This\n // path builds an AST, so it had nothing to echo, and the `sql` field simply\n // vanished from the response whenever a query was date-bucketed (native SQL\n // declines granularity, handing those queries here). An author reading the\n // response then couldn't tell \"bucketing is not implemented\" from \"this\n // strategy doesn't report\". Best-effort: rendering is a debugging aid and\n // must never fail a query that already ran.\n let sql: string | undefined;\n try {\n sql = (await this.generateSql(query, ctx)).sql;\n } catch {\n sql = undefined;\n }\n return sql ? { rows: mappedRows, fields, sql } : { rows: mappedRows, fields };\n }\n\n /**\n * Render a REPRESENTATIVE SQL string for an ObjectQL aggregate query.\n *\n * This path executes through `engine.aggregate()`, not raw SQL, so the string\n * is documentation rather than the literal statement — but it must be an\n * honest account of what the query does, because dataset responses echo it\n * and authors read it to verify their widget options landed (#3588). It\n * therefore renders date bucketing (`date_trunc`), the WHERE predicate,\n * ordering, and the row window.\n *\n * Filter VALUES are rendered as `$n` placeholders and returned in `params`,\n * never inlined: the echoed statement travels to the browser, and a filter\n * comparand can carry tenant data.\n */\n async generateSql(query: AnalyticsQuery, ctx: StrategyContext): Promise<{ sql: string; params: unknown[] }> {\n const cube = ctx.getCube(query.cube!);\n if (!cube) {\n throw new Error(`Cube not found: ${query.cube}`);\n }\n\n const selectParts: string[] = [];\n const groupByParts: string[] = [];\n const params: unknown[] = [];\n\n // Date-bucketed dimensions render as `date_trunc('<granularity>', col)` —\n // the SQL shape the driver's own bucketing implements — so a `month` trend\n // no longer reads as if it grouped by the raw column.\n const granByDim = new Map<string, string>();\n for (const td of query.timeDimensions ?? []) {\n if (td.granularity) granByDim.set(td.dimension, td.granularity);\n }\n const tableName = this.extractObjectName(cube);\n // #3654 — plan cross-object dims (throws for out-of-envelope, so\n // `/analytics/sql` and `execute()` accept/reject the SAME set). An in-envelope\n // cross-object dim renders as a LEFT JOIN — its logical shape; `execute()`\n // serves it via FK-expand.\n const plan = this.planCrossObject(cube, query, Object.fromEntries(\n normalizeAnalyticsFilters(query).map((f) => [this.resolveFieldName(cube, f.member, 'any'), true]),\n ));\n const crossByDim = new Map((plan?.crossDims ?? []).map((cd) => [cd.outputName, cd]));\n const joinClauses: string[] = [];\n const dimExpr = (dim: string): string => {\n const cd = crossByDim.get(dim);\n if (cd) {\n joinClauses.push(\n `LEFT JOIN \"${cd.refObject}\" ON \"${tableName}\".\"${cd.fkField}\" = \"${cd.refObject}\".\"id\"`,\n );\n return `\"${cd.refObject}\".\"${cd.attr}\"`;\n }\n const col = this.resolveFieldName(cube, dim, 'dimension');\n const gran = granByDim.get(dim);\n return gran ? `date_trunc('${gran}', ${col})` : col;\n };\n\n if (query.dimensions) {\n for (const dim of query.dimensions) {\n const expr = dimExpr(dim);\n selectParts.push(`${expr} AS \"${dim}\"`);\n groupByParts.push(expr);\n }\n }\n // A time dimension that is bucketed but not also listed in `dimensions`\n // still groups (see `execute`), so it belongs in the rendered GROUP BY too.\n for (const [dim] of granByDim) {\n if (query.dimensions?.includes(dim)) continue;\n const expr = dimExpr(dim);\n selectParts.push(`${expr} AS \"${dim}\"`);\n groupByParts.push(expr);\n }\n if (query.measures) {\n for (const m of query.measures) {\n const { field, method } = this.resolveMeasureAggregation(cube, m);\n const aggSql = method === 'count'\n ? 'COUNT(*)'\n : method === 'count_distinct'\n ? `COUNT(DISTINCT ${field})`\n : `${method.toUpperCase()}(${field})`;\n selectParts.push(`${aggSql} AS \"${m}\"`);\n }\n }\n\n // ADR-0021 D-C (#3602) — render the READ SCOPE too, not just the caller's\n // own filters (#3652 added those). Without it this string still reads as an\n // unscoped table scan while the real aggregate is scoped (#3601), so anyone\n // debugging a \"why is this row missing\" gets SQL that cannot reproduce the\n // result. Nothing leaks — the string is never executed, and scope VALUES\n // stay in `params`, which `execute()`'s echo discards — but a rendering\n // that contradicts execution is worse than no rendering.\n //\n // The cross-object guard runs here for the same reason: this must not\n // render SQL for a query `execute()` would reject outright (#3654).\n //\n // Faithfulness cuts both ways: the time-dimension WINDOWS render too, from\n // the same `dateRangeBounds` lowering `execute()` sends to the engine\n // (#3650). This comment used to explain why a BETWEEN was deliberately\n // absent — because `execute()` dropped the window and rendering one would\n // have invented a predicate. Now that it applies the window, omitting it\n // here would be the lie in the other direction.\n // (The cross-object envelope was already enforced by `planCrossObject` above,\n // so `/analytics/sql` rejects the same out-of-envelope set `execute()` does.)\n\n const whereParts: string[] = [];\n for (const f of normalizeAnalyticsFilters(query)) {\n const clause = this.buildFilterClauseSql(\n this.resolveFieldName(cube, f.member, 'any'),\n f.operator,\n f.values,\n params,\n );\n if (clause) whereParts.push(clause);\n }\n // Bounds bind as `$n` placeholders like every other comparand: this string\n // travels to the browser, and a window can carry tenant-derived dates.\n for (const { field, bounds } of this.dateRangeBounds(cube, query)) {\n params.push(bounds.$gte, bounds.$lte);\n whereParts.push(`${field} BETWEEN $${params.length - 1} AND $${params.length}`);\n }\n // Read scope last, so it reads as the outermost constraint. Compiled by the\n // same fail-closed compiler `NativeSQLStrategy` uses — it throws rather than\n // drop a predicate, which is the correct posture even for a display string:\n // silently omitting the scope is exactly the misleading output being fixed.\n const scope = ctx.getReadScope?.(tableName);\n if (scope != null) {\n const { sql: scopeSql, params: scopeParams } = compileScopedFilterToSql(scope, tableName);\n if (scopeSql) {\n let i = 0;\n // `compileScopedFilterToSql` emits `?`; renumber into this builder's $N.\n const rendered = scopeSql.replace(/\\?/g, () => {\n params.push(scopeParams[i++]);\n return `$${params.length}`;\n });\n whereParts.push(`(${rendered})`);\n }\n }\n\n let sql = `SELECT ${selectParts.join(', ')} FROM \"${tableName}\"`;\n if (joinClauses.length > 0) sql += ' ' + joinClauses.join(' ');\n if (whereParts.length > 0) {\n sql += ` WHERE ${whereParts.join(' AND ')}`;\n }\n if (groupByParts.length > 0) {\n sql += ` GROUP BY ${groupByParts.join(', ')}`;\n }\n if (query.order && Object.keys(query.order).length > 0) {\n const orderClauses = Object.entries(query.order).map(([f, d]) => `\"${f}\" ${d.toUpperCase()}`);\n sql += ` ORDER BY ${orderClauses.join(', ')}`;\n }\n if (query.limit != null) sql += ` LIMIT ${query.limit}`;\n if (query.offset != null) sql += ` OFFSET ${query.offset}`;\n\n return { sql, params };\n }\n\n // ── Helpers ──────────────────────────────────────────────────────\n\n /**\n * ADR-0021 D-C (#3597) — AND the object's read scope (tenant + RLS) into the\n * filter handed to `engine.aggregate`.\n *\n * This path used to drop the scope entirely, and the engine could not make up\n * for it: the aggregate bridge passes no `ExecutionContext`, so the security\n * middleware's principal-less fall-open skipped its own RLS injection. Both\n * belts were off at once — an authenticated caller received aggregates\n * computed over EVERY tenant's rows.\n *\n * Composed with `$and`, never by key merge: the query's own filter and the\n * scope can name the SAME field (e.g. a dashboard filtering `organization_id`),\n * and a spread would let caller input silently overwrite the security\n * predicate. `$and` makes that structurally impossible.\n */\n private withReadScope(\n objectName: string,\n filter: Record<string, unknown>,\n ctx: StrategyContext,\n ): Record<string, unknown> | undefined {\n const userFilter = Object.keys(filter).length > 0 ? filter : undefined;\n if (typeof ctx.getReadScope !== 'function') return userFilter;\n const scope = ctx.getReadScope(objectName);\n if (scope === undefined || scope === null) return userFilter;\n const scopeFilter = scope as Record<string, unknown>;\n if (!userFilter) return scopeFilter;\n return { $and: [userFilter, scopeFilter] };\n }\n\n /** Is `field` a resolved cross-object (relationship-traversal) reference? */\n private isCrossObjectField(cube: Cube, field: string, baseObject: string): boolean {\n if (!field.includes('.')) return false;\n const alias = field.split('.')[0];\n const joinedObject = cube.joins?.[alias]?.name ?? alias;\n return joinedObject !== baseObject;\n }\n\n /**\n * Plan how to serve cross-object references on this join-less path (#3654).\n *\n * `engine.aggregate()` cannot join. A cross-object DIMENSION within a\n * supported envelope is served by an FK-expand (`executeCrossObject`): group\n * the base aggregate on the lookup FK, resolve the FK to the related attribute\n * with a SCOPED read, re-bucket in memory. Returns `null` for a base-only\n * query (direct path), a plan for an in-envelope cross-object query.\n *\n * THROWS for anything outside the envelope — a cross-object MEASURE or FILTER\n * (needs a real join to evaluate), a MULTI-HOP dimension (`a.b.c`), or a\n * non-recombinable measure (`avg`/`count_distinct`, whose sub-bucket values\n * cannot be merged). A loud error beats the silent mis-bucket #3654 kills.\n * `generateSql()` calls this too, so the preview accepts/rejects the same set.\n *\n * Detection is on RESOLVED field names, so a dotted dimension the cube\n * flattens to a real column is treated as base, not cross-object.\n */\n private planCrossObject(\n cube: Cube,\n query: AnalyticsQuery,\n filter: Record<string, unknown>,\n ): CrossObjectPlan | null {\n const baseObject = this.extractObjectName(cube);\n\n // A date bucket over a related object's field is not supported. Checked\n // FIRST: since #3650 a `dateRange` also lands in `filter`, so a cross-object\n // time dimension would otherwise be reported as a \"cross-object filter\" —\n // true of the lowered predicate, but not what the author wrote.\n for (const td of query.timeDimensions ?? []) {\n const field = this.resolveFieldName(cube, td.dimension, 'dimension');\n if (this.isCrossObjectField(cube, field, baseObject)) {\n throw new Error(\n `[Analytics] ObjectQLStrategy cannot bucket a cross-object time dimension (\"${field}\").`,\n );\n }\n }\n\n // A cross-object MEASURE or FILTER can only be evaluated with a real join.\n const nonDim = [\n ...(query.measures ?? []).map((m) => ({ where: 'measure', field: this.resolveMeasureAggregation(cube, m).field })),\n ...Object.keys(filter).map((f) => ({ where: 'filter', field: f })),\n ].filter((r) => this.isCrossObjectField(cube, r.field, baseObject));\n if (nonDim.length > 0) {\n throw new Error(\n `[Analytics] ObjectQLStrategy cannot evaluate a cross-object ${nonDim[0].where} ` +\n `(\"${nonDim[0].field}\") — the engine cannot join in an aggregate. Run this ` +\n `query on a native-SQL driver, or remove the cross-object ${nonDim[0].where}.`,\n );\n }\n\n // Collect cross-object DIMENSIONS (single-hop only).\n const crossDims: CrossObjectPlanDim[] = [];\n for (const dim of query.dimensions ?? []) {\n const field = this.resolveFieldName(cube, dim, 'dimension');\n if (!this.isCrossObjectField(cube, field, baseObject)) continue;\n const [alias, ...rest] = field.split('.');\n const attr = rest.join('.');\n if (attr.includes('.')) {\n throw new Error(\n `[Analytics] ObjectQLStrategy supports only single-hop cross-object ` +\n `dimensions; \"${field}\" traverses more than one relationship.`,\n );\n }\n crossDims.push({ outputName: dim, fkField: alias, attr, refObject: cube.joins?.[alias]?.name ?? alias });\n }\n\n if (crossDims.length === 0) return null;\n\n // Every measure must re-combine across the intermediate FK sub-buckets.\n for (const m of query.measures ?? []) {\n const { method } = this.resolveMeasureAggregation(cube, m);\n if (!RECOMBINABLE_METHODS.has(method)) {\n throw new Error(\n `[Analytics] ObjectQLStrategy cannot group by a cross-object dimension ` +\n `with a \"${method}\" measure (\"${m}\") — its value cannot be recombined ` +\n `across the intermediate FK grouping. Use sum/count/min/max, or run on ` +\n `a native-SQL driver.`,\n );\n }\n }\n\n return { crossDims };\n }\n\n /**\n * Serve a cross-object-dimension query by FK-expand (#3654). The pure\n * re-bucketing step lives in `cross-object-rebucket.ts`.\n */\n private async executeCrossObject(\n cube: Cube,\n query: AnalyticsQuery,\n aggregations: Array<{ field: string; method: string; alias: string }>,\n filter: Record<string, unknown>,\n plan: CrossObjectPlan,\n ctx: StrategyContext,\n ): Promise<AnalyticsResult> {\n const baseObject = this.extractObjectName(cube);\n const crossByDim = new Map(plan.crossDims.map((cd) => [cd.outputName, cd]));\n\n // Rewrite group-by: a cross-object dim becomes its base FK column; base and\n // time dims pass through. `baseDimFields` are the group keys carried into\n // the re-bucket verbatim (the FK columns are replaced by resolved attrs).\n type GroupByItem = string | { field: string; dateGranularity: string };\n const granByDim = new Map<string, string>();\n for (const td of query.timeDimensions ?? []) {\n if (td.granularity) granByDim.set(td.dimension, td.granularity);\n }\n const groupBy: GroupByItem[] = [];\n const baseDimFields: string[] = [];\n for (const dim of query.dimensions ?? []) {\n const cd = crossByDim.get(dim);\n if (cd) {\n groupBy.push(cd.fkField);\n continue;\n }\n const field = this.resolveFieldName(cube, dim, 'dimension');\n const gran = granByDim.get(dim);\n groupBy.push(gran ? { field, dateGranularity: gran } : field);\n baseDimFields.push(field);\n granByDim.delete(dim);\n }\n for (const [dim, gran] of granByDim) {\n const field = this.resolveFieldName(cube, dim, 'dimension');\n groupBy.push({ field, dateGranularity: gran });\n baseDimFields.push(field);\n }\n\n // Base aggregate, grouped by the FK, scoped to the base object. Threads the\n // ExecutionContext for the engine-side second belt too (#3602).\n const baseRows = await ctx.executeAggregate!(baseObject, {\n groupBy: groupBy.length > 0 ? (groupBy as unknown as string[]) : undefined,\n aggregations: aggregations.length > 0 ? aggregations : undefined,\n filter: this.withReadScope(baseObject, filter, ctx),\n timezone: query.timezone,\n context: ctx.context,\n });\n\n // Resolve each cross-object dim's FK → attribute, SCOPED to the referenced\n // object: a related record the caller cannot read never yields its\n // attribute, so it buckets as RESTRICTED (no leak; ADR-0021 D-C / #3602).\n const resolvedDims: CrossObjectDim[] = [];\n for (const cd of plan.crossDims) {\n const fkValues = [...new Set(baseRows.map((r) => r[cd.fkField]).filter((v) => v != null))];\n const fkToAttr = await this.resolveFkAttr(cd.refObject, cd.attr, fkValues, ctx);\n resolvedDims.push({ outputName: cd.outputName, fkField: cd.fkField, fkToAttr });\n }\n\n const measures: MeasureRecombine[] = (query.measures ?? []).map((m) => ({\n alias: m,\n // planCrossObject already asserted every measure is recombinable.\n method: this.resolveMeasureAggregation(cube, m).method as RecombinableMethod,\n }));\n\n const merged = rebucketCrossObject(baseRows, baseDimFields, resolvedDims, measures);\n\n // Map resolved group keys back to the caller's dimension names.\n const mappedRows = merged.map((row) => {\n const out: Record<string, unknown> = {};\n for (const dim of query.dimensions ?? []) {\n if (crossByDim.has(dim)) {\n if (dim in row) out[dim] = row[dim];\n } else {\n const field = this.resolveFieldName(cube, dim, 'dimension');\n if (field in row) out[dim] = row[field];\n }\n }\n for (const td of query.timeDimensions ?? []) {\n if (query.dimensions?.includes(td.dimension)) continue;\n const field = this.resolveFieldName(cube, td.dimension, 'dimension');\n if (field in row) out[td.dimension] = row[field];\n }\n for (const m of query.measures ?? []) {\n if (m in row) out[m] = row[m];\n }\n return out;\n });\n\n return { rows: mappedRows, fields: this.buildFieldMeta(query, cube) };\n }\n\n /**\n * Resolve `fkValues` (ids of `refObject`) to their `attr` values, applying the\n * referenced object's OWN read scope (#3654 / #3602). Reuses the aggregate\n * bridge — `group by (id, attr)` is one row per record. Ids the scope hides\n * are simply absent from the map (⇒ RESTRICTED bucket downstream).\n */\n private async resolveFkAttr(\n refObject: string,\n attr: string,\n fkValues: unknown[],\n ctx: StrategyContext,\n ): Promise<Map<unknown, unknown>> {\n const map = new Map<unknown, unknown>();\n if (fkValues.length === 0 || typeof ctx.executeAggregate !== 'function') return map;\n const idFilter: Record<string, unknown> = { id: { $in: fkValues } };\n const scope = typeof ctx.getReadScope === 'function' ? ctx.getReadScope(refObject) : null;\n const filter = scope != null ? { $and: [idFilter, scope] } : idFilter;\n const rows = await ctx.executeAggregate(refObject, {\n groupBy: ['id', attr],\n aggregations: [{ field: 'id', method: 'count', alias: '_c' }],\n filter,\n context: ctx.context,\n });\n for (const r of rows) {\n if (r.id != null) map.set(r.id, r[attr]);\n }\n return map;\n }\n\n /**\n * Render one normalized filter as a display SQL predicate for `generateSql`.\n *\n * Mirrors `NativeSQLStrategy.buildFilterClause`'s operator vocabulary so the\n * two previews read alike, but binds through `coerceFilterValueForObjectQL`:\n * the comparand shown is the one THIS path actually hands the engine (a real\n * boolean, not SQL's 1/0). Returns null for an operator/value combination\n * that carries no predicate, matching `execute()`, which drops it too.\n */\n private buildFilterClauseSql(\n col: string,\n operator: string,\n values: string[] | undefined,\n params: unknown[],\n ): string | null {\n if (operator === 'set') return `${col} IS NOT NULL`;\n if (operator === 'notSet') return `${col} IS NULL`;\n\n if (!values || values.length === 0) return null;\n\n if (operator === 'in' || operator === 'notIn') {\n const placeholders = values\n .map((v) => { params.push(coerceFilterValueForObjectQL(v)); return `$${params.length}`; })\n .join(', ');\n return `${col} ${operator === 'in' ? 'IN' : 'NOT IN'} (${placeholders})`;\n }\n\n if (operator === 'contains' || operator === 'notContains') {\n params.push(`%${values[0]}%`);\n return `${col} ${operator === 'contains' ? 'LIKE' : 'NOT LIKE'} $${params.length}`;\n }\n\n const op = SCALAR_SQL_OPS[operator];\n if (!op) return null;\n params.push(coerceFilterValueForObjectQL(values[0]));\n return `${col} ${op} $${params.length}`;\n }\n\n /**\n * Resolve a member ref to a `{ sql, type? }` definition.\n *\n * Mirrors `NativeSQLStrategy.lookupMember` so the two strategies\n * accept the same naming conventions:\n * 1. `<cube>.<field>` — canonical analytics qualifier.\n * 2. `<lookup>.<field>` — relation traversal (e.g. `account.industry`).\n * Tries literal key, then underscore-flattened key, then falls\n * back to a synthetic dim whose `sql` is the dotted path so the\n * ObjectQL aggregate engine can traverse it via the lookup field.\n * 3. `<field>` — bare column on the cube's table.\n */\n private lookupMember(\n cube: Cube,\n member: string,\n kind: 'dimension' | 'measure',\n ): { sql: string; type?: string } | undefined {\n const bag = kind === 'dimension' ? cube.dimensions : cube.measures;\n if (bag[member]) return bag[member];\n if (member.includes('.')) {\n const [first, ...rest] = member.split('.');\n const tail = rest.join('.');\n if (first === cube.name && bag[tail]) return bag[tail];\n if (bag[tail]) return bag[tail];\n const flat = member.replace(/\\./g, '_');\n if (bag[flat]) return bag[flat];\n if (kind === 'dimension') return { sql: member, type: 'string' };\n } else if (bag[member]) {\n return bag[member];\n }\n return undefined;\n }\n\n private resolveFieldName(cube: Cube, member: string, kind: 'dimension' | 'measure' | 'any'): string {\n if (kind === 'dimension' || kind === 'any') {\n const dim = this.lookupMember(cube, member, 'dimension');\n if (dim) return dim.sql.replace(/^\\$/, '');\n }\n if (kind === 'measure' || kind === 'any') {\n const measure = this.lookupMember(cube, member, 'measure');\n if (measure) return measure.sql.replace(/^\\$/, '');\n }\n return member.includes('.') ? member.split('.')[1] : member;\n }\n\n private resolveMeasureAggregation(cube: Cube, measureName: string): { field: string; method: string } {\n const direct = this.lookupMember(cube, measureName, 'measure') as\n | { sql: string; type: string }\n | undefined;\n if (direct) {\n return {\n field: direct.sql.replace(/^\\$/, ''),\n method: direct.type === 'count_distinct' ? 'count_distinct' : direct.type,\n };\n }\n // Accept `${field}_${type}` aliases (e.g. 'amount_sum') for measures whose\n // canonical name is just `${field}` (e.g. measure 'amount' of type 'sum').\n // This matches the convention used by clients that build measure names\n // from (field, function) pairs (e.g. the data-objectstack adapter).\n const fieldName = measureName.includes('.') ? measureName.split('.')[1] : measureName;\n const aggTypes = ['count', 'sum', 'avg', 'min', 'max', 'count_distinct'];\n for (const type of aggTypes) {\n const suffix = `_${type}`;\n if (fieldName.endsWith(suffix)) {\n const baseField = fieldName.slice(0, -suffix.length);\n const candidate = cube.measures[baseField];\n if (candidate && candidate.type === type) {\n return {\n field: candidate.sql.replace(/^\\$/, ''),\n method: candidate.type === 'count_distinct' ? 'count_distinct' : candidate.type,\n };\n }\n }\n }\n return { field: '*', method: 'count' };\n }\n\n /**\n * AND one more operand onto `filter[field]`, merging operator objects rather\n * than overwriting them. Returns a standalone conjunct when the two cannot\n * share one entry, or `null` when the merge absorbed the operand.\n *\n * Every predicate this strategy contributes goes through here — the caller's\n * `where` and the time-dimension `dateRange` alike. Two operands on one field\n * are the normal case (`{$gte}` from a `where` plus `{$gte,$lte}` from a\n * window on `close_date`), and a plain assignment would keep only the last:\n * that is how a range used to lose a bound.\n *\n * Spreading is sound only while the operands name DIFFERENT operators. Where\n * they collide — two `$gte` bounds on one field, which a window makes routine\n * and which a `where` can already produce on its own through `$and` — the\n * spread keeps whichever came last and WIDENS the query. Same for a bare\n * equality meeting an operator object: neither can absorb the other. Those\n * are handed back for the caller to AND in separately, so the engine\n * intersects them instead of the strategy picking a winner.\n */\n private mergeFilterOperand(\n filter: Record<string, unknown>,\n field: string,\n operand: unknown,\n ): Record<string, unknown> | null {\n const existing = filter[field];\n if (existing === undefined) {\n filter[field] = operand;\n return null;\n }\n const mergeable = (v: unknown): v is Record<string, unknown> =>\n !!v && typeof v === 'object' && !Array.isArray(v);\n if (!mergeable(existing) || !mergeable(operand)) return { [field]: operand };\n if (Object.keys(operand).some((op) => op in existing)) return { [field]: operand };\n filter[field] = { ...existing, ...operand };\n return null;\n }\n\n /**\n * Lower `timeDimensions[].dateRange` into resolved-field bounds (#3650).\n *\n * `dateRange` states a WINDOW on a time dimension; it is a SIBLING of `where`,\n * never folded into it. `normalizeAnalyticsFilters` reads only `where`, so\n * this path used to drop the window on the floor — no error, just every row\n * ever recorded. Nor is that a corner case: `NativeSQLStrategy.canHandle`\n * declines any query carrying a `granularity`, so a date-bucketed trend lands\n * HERE on every driver — and \"bucketed trend\" is precisely the shape that also\n * carries a range (\"last 12 months\", \"this quarter\").\n *\n * Bounds are inclusive on both ends — the same `$gte`/`$lte` pair\n * `NativeSQLStrategy` binds as `BETWEEN` and the memory driver builds as a\n * `$match`, so one dashboard reads the same on every driver.\n *\n * Comparands are coerced by the SAME helper the `where` path uses, so an\n * epoch-ms bound recovers as a number and an ISO string stays a string. No\n * STORAGE coercion happens here, deliberately: `NativeSQLStrategy` needs\n * `coerceTemporal` because it binds into raw SQL and had to learn that a\n * SQLite `Field.datetime` is an INTEGER epoch (#2034); this path goes through\n * `engine.aggregate()`, where the driver's own CRUD filter coercion applies —\n * the very coercion that already makes a `where` bound on that same column\n * work today.\n *\n * A bare-string `dateRange` degenerates to the single point `[s, s]`, matching\n * `NativeSQLStrategy`. Relative phrases (\"Last 7 days\") are NOT resolved here;\n * neither SQL path resolves them, and inventing a second interpretation on the\n * driver-independent path is how the two would drift apart again.\n *\n * An oddly-sized array (the schema types `dateRange` as a plain `string[]`)\n * takes its first two entries, a one-entry array degenerating to a point.\n * `NativeSQLStrategy` drops such a window entirely — but \"drop the window\"\n * means \"plot all of history\", which is the very failure this fixes, so the\n * fallback here errs toward the narrower query instead.\n */\n private dateRangeBounds(\n cube: Cube,\n query: AnalyticsQuery,\n ): Array<{ field: string; bounds: Record<string, unknown> }> {\n const out: Array<{ field: string; bounds: Record<string, unknown> }> = [];\n for (const td of query.timeDimensions ?? []) {\n if (!td.dateRange) continue;\n const range = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];\n const [start, end = start] = range;\n if (start == null) continue;\n out.push({\n field: this.resolveFieldName(cube, td.dimension, 'dimension'),\n bounds: {\n $gte: coerceFilterValueForObjectQL(String(start)),\n $lte: coerceFilterValueForObjectQL(String(end)),\n },\n });\n }\n return out;\n }\n\n private convertFilter(operator: string, values?: string[]): unknown {\n if (operator === 'set') return { $ne: null };\n if (operator === 'notSet') return null;\n if (!values || values.length === 0) return undefined;\n\n const v0 = coerceFilterValueForObjectQL(values[0]);\n const all = values.map(coerceFilterValueForObjectQL);\n switch (operator) {\n case 'equals': return v0;\n case 'notEquals': return { $ne: v0 };\n case 'gt': return { $gt: v0 };\n case 'gte': return { $gte: v0 };\n case 'lt': return { $lt: v0 };\n case 'lte': return { $lte: v0 };\n case 'contains': return { $regex: values[0] };\n case 'in': return { $in: all };\n case 'notIn': return { $nin: all };\n default: return v0;\n }\n }\n\n private extractObjectName(cube: Cube): string {\n return cube.sql.trim();\n }\n\n private buildFieldMeta(query: AnalyticsQuery, cube: Cube): Array<{ name: string; type: string }> {\n const fields: Array<{ name: string; type: string }> = [];\n if (query.dimensions) {\n for (const dim of query.dimensions) {\n const d = this.lookupMember(cube, dim, 'dimension');\n fields.push({ name: dim, type: d?.type || 'string' });\n }\n }\n if (query.measures) {\n for (const m of query.measures) {\n fields.push({ name: m, type: 'number' });\n }\n }\n return fields;\n }\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Cube, Metric, Dimension as CubeDimension, CubeJoin } from '@objectstack/spec/data';\nimport type { Dataset, DatasetMeasure, DatasetDimension } from '@objectstack/spec/ui';\nimport type { FilterCondition } from '@objectstack/spec/data';\n\n/**\n * Dataset → Cube compiler (ADR-0021 D-A=(c), WS2).\n *\n * Lowers a declarative `dataset` (base object + included relationships +\n * declared dimensions/measures + derived measures) into the existing Cube\n * analytics runtime model. The author never writes an `ON` clause: joins are\n * DERIVED from the `include` relationship names and the dotted `relationship.field`\n * references on dimensions/measures, matching the NativeSQLStrategy convention\n * `<parentTable>.<relationship> = <relationship>.id`.\n *\n * Safety (D-C): every dotted field reference must point at a relationship that\n * the dataset explicitly declared in `include`; otherwise the compile fails.\n * The returned `allowedRelationships` set is the join allowlist the strategy\n * enforces at SQL-build time.\n */\n\n/** Operators v1 does NOT compile to the Cube SQL switch — surfaced as a clear error. */\nconst UNSUPPORTED_AGGREGATES = new Set(['array_agg', 'string_agg']);\n\nexport interface DerivedMeasureSpec {\n name: string;\n op: 'ratio' | 'sum' | 'difference' | 'product';\n of: string[];\n}\n\nexport interface CompiledDataset {\n /** The Cube the dataset compiles to (consumed by the strategy chain). */\n cube: Cube;\n /**\n * Every join alias the dataset may use — each declared `include` path AND its\n * intermediate prefixes (ADR-0071). The join allowlist (D-C): the\n * NativeSQLStrategy rejects any join alias not in this set.\n */\n allowedRelationships: Set<string>;\n /** Derived measures, computed post-aggregation by the executor (Q1). */\n derived: DerivedMeasureSpec[];\n /** Definition-level filter (the dataset's intrinsic scope). */\n filter?: FilterCondition;\n /** Per-measure scoped filters, keyed by measure name (applied by executor). */\n measureFilters: Record<string, FilterCondition>;\n}\n\n/**\n * The related object reached by traversing a relationship: its logical object\n * name (used to resolve the NEXT hop in a multi-hop chain — ADR-0071) and its\n * physical table name (the join target).\n */\nexport interface RelationshipTarget {\n object: string;\n table: string;\n}\n\n/**\n * Resolves a relationship name on a base object to the related object/table,\n * using the runtime's object graph. Optional: when omitted the compiler trusts\n * the declared `include` names (the NativeSQLStrategy convention assumes the\n * relationship name equals the related table name).\n *\n * May return a bare table-name `string` (legacy single-hop: object name is\n * assumed equal to the table) or a {@link RelationshipTarget} (required to\n * traverse further along a multi-hop path, where object differs from table for\n * namespaced objects).\n */\nexport type RelationshipResolver = (\n baseObject: string,\n relationshipName: string,\n) => string | RelationshipTarget | undefined;\n\n/** Map a dataset measure's aggregate to the Cube metric `type`. */\nfunction aggregateToMetricType(m: DatasetMeasure): Metric['type'] {\n // Only reached for non-derived measures, where the spec refinement guarantees\n // an aggregate; guard defensively so the type narrows from `optional`.\n if (!m.aggregate) {\n throw new Error(`[dataset-compiler] non-derived measure \"${m.name}\" has no aggregate`);\n }\n if (UNSUPPORTED_AGGREGATES.has(m.aggregate)) {\n throw new Error(\n `[dataset-compiler] measure \"${m.name}\" uses aggregate \"${m.aggregate}\" which is ` +\n `not supported by the v1 dataset runtime (supported: count, sum, avg, min, max, count_distinct).`,\n );\n }\n return m.aggregate as Metric['type'];\n}\n\n/** Map a dataset dimension type to the Cube dimension `type`. */\nfunction dimensionType(d: DatasetDimension): CubeDimension['type'] {\n switch (d.type) {\n case 'date': return 'time';\n case 'number': return 'number';\n case 'boolean': return 'boolean';\n case 'lookup': return 'string';\n case 'string': return 'string';\n default: return 'string';\n }\n}\n\n/** The relationship PATH a dotted field traverses — all segments but the final\n * column — or null for a base-object field. E.g. `account.owner.region` →\n * `account.owner`; `account.region` → `account`; `region` → null. */\nfunction fieldRelationshipPath(field: string): string | null {\n const idx = field.lastIndexOf('.');\n return idx > 0 ? field.slice(0, idx) : null;\n}\n\n/** Max relationship hops in one `include` path — base → 3 hops = 4 objects\n * (ADR-0071; Salesforce-report-type parity). To-one chains never fan out, so\n * this is a performance/complexity guard, not a correctness limit. */\nconst MAX_JOIN_HOPS = 3;\n\n/** SQL-safe join alias for a relationship PATH. The dotted path is the author-\n * facing form; the alias replaces dots with `__` (Cube.js convention) so each\n * prefix is one valid identifier — quoted dotted identifiers are rejected by\n * the read-scope SQL guard (fail-closed). Single-segment paths are unchanged,\n * so single-hop joins stay byte-for-byte identical. */\nconst joinAlias = (path: string): string => path.replace(/\\./g, '__');\n\nexport function compileDataset(\n dataset: Dataset,\n resolver?: RelationshipResolver,\n): CompiledDataset {\n const include = dataset.include ?? [];\n\n // Resolve each declared relationship PATH into its ordered join chain, emitting\n // one Cube join per PATH PREFIX (ADR-0071 multi-hop, to-one only). The join\n // ALIAS is the full dotted path (`account.owner`), which self-describes the\n // chain: the parent alias is the path minus its last segment, the FK column is\n // that last segment. So declaring `account.owner` auto-adds the intermediate\n // `account` join, and the strategy can rebuild every `ON` from the alias alone.\n // Without a resolver, each segment's relationship name is assumed to equal both\n // the related object and its table (legacy convention / unit tests).\n const resolveHop = (fromObject: string, rel: string): RelationshipTarget => {\n if (!resolver) return { object: rel, table: rel };\n const resolved = resolver(fromObject, rel);\n if (!resolved) {\n throw new Error(\n `[dataset-compiler] dataset \"${dataset.name}\" includes relationship \"${rel}\" ` +\n `which does not exist on object \"${fromObject}\".`,\n );\n }\n return typeof resolved === 'string' ? { object: resolved, table: resolved } : resolved;\n };\n const joins: Record<string, CubeJoin> = {};\n for (const path of include) {\n const segments = path.split('.');\n if (segments.length > MAX_JOIN_HOPS) {\n throw new Error(\n `[dataset-compiler] dataset \"${dataset.name}\" include path \"${path}\" exceeds the ` +\n `${MAX_JOIN_HOPS}-hop limit (${segments.length} hops). Deeper traversal is not supported.`,\n );\n }\n let fromObject = dataset.object;\n let parentAlias = dataset.object;\n let prefix = '';\n for (const seg of segments) {\n prefix = prefix ? `${prefix}.${seg}` : seg;\n const target = resolveHop(fromObject, seg);\n const alias = joinAlias(prefix);\n if (!joins[alias]) {\n // KEY is the SQL-safe alias; `name` carries the join TABLE; the strategy\n // rebuilds the ON clause from the alias convention (`<parent>.<seg> = <alias>.id`).\n joins[alias] = {\n name: target.table,\n relationship: 'many_to_one',\n sql: `${parentAlias}.${seg} = ${prefix}.id`,\n };\n }\n fromObject = target.object;\n parentAlias = prefix;\n }\n }\n\n // The join allowlist (D-C) is every registered alias — each declared path AND\n // its intermediate prefixes — so a multi-hop field's intermediate joins pass.\n const allowedRelationships = new Set(Object.keys(joins));\n\n // Assert any dotted field only traverses a DECLARED relationship PATH (D-C).\n const assertDeclared = (field: string, ownerKind: string, ownerName: string) => {\n const relPath = fieldRelationshipPath(field);\n if (relPath && !joins[joinAlias(relPath)]) {\n throw new Error(\n `[dataset-compiler] ${ownerKind} \"${ownerName}\" references relationship path \"${relPath}\" ` +\n `via \"${field}\", but \"${relPath}\" is not declared in the dataset's \\`include\\`. ` +\n `Only fields along a declared relationship path are joinable.`,\n );\n }\n };\n\n // Compile dimensions.\n const dimensions: Record<string, CubeDimension> = {};\n for (const d of dataset.dimensions) {\n assertDeclared(d.field, 'dimension', d.name);\n const dim: CubeDimension = {\n name: d.name,\n label: typeof d.label === 'string' ? d.label : d.name,\n type: dimensionType(d),\n sql: d.field,\n };\n if (dim.type === 'time') {\n dim.granularities = d.dateGranularity\n ? [d.dateGranularity]\n : ['day', 'week', 'month', 'quarter', 'year'];\n }\n dimensions[d.name] = dim;\n }\n\n // Compile measures (non-derived → Cube metrics; derived → sidecar).\n const measures: Record<string, Metric> = {};\n const derived: DerivedMeasureSpec[] = [];\n const measureFilters: Record<string, FilterCondition> = {};\n\n for (const m of dataset.measures) {\n if (m.derived) {\n derived.push({ name: m.name, op: m.derived.op, of: m.derived.of });\n continue;\n }\n if (m.field) assertDeclared(m.field, 'measure', m.name);\n const metric: Metric = {\n name: m.name,\n label: typeof m.label === 'string' ? m.label : m.name,\n type: aggregateToMetricType(m),\n // `count` with no field aggregates over rows (*).\n sql: m.field ?? '*',\n };\n if (typeof m.format === 'string') metric.format = m.format;\n measures[m.name] = metric;\n if (m.filter) measureFilters[m.name] = m.filter;\n }\n\n const cube: Cube = {\n name: dataset.name,\n title: typeof dataset.label === 'string' ? dataset.label : dataset.name,\n sql: dataset.object,\n measures,\n dimensions,\n public: false,\n };\n if (Object.keys(joins).length > 0) cube.joins = joins;\n\n return {\n cube,\n allowedRelationships,\n derived,\n filter: dataset.filter,\n measureFilters,\n };\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n IAnalyticsService,\n AnalyticsQuery,\n AnalyticsResult,\n DatasetSelection,\n DatasetCompareTo,\n} from '@objectstack/spec/contracts';\nimport type { FilterCondition } from '@objectstack/spec/data';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\nimport { filterTokenContextFrom, resolveFilterTokens } from '@objectstack/core';\nimport type { CompiledDataset, DerivedMeasureSpec } from './dataset-compiler.js';\nimport type { OrderLabelResolver } from './dimension-labels.js';\n\n// Re-export the shared protocol shapes so existing importers keep working.\nexport type { DatasetSelection } from '@objectstack/spec/contracts';\n/** @deprecated use DatasetCompareTo from @objectstack/spec/contracts */\nexport type CompareTo = DatasetCompareTo;\n\n/**\n * Dataset executor (ADR-0021 WS2).\n *\n * Turns a compiled dataset + a presentation's selection (dimensions, measures,\n * runtime filter, compareTo) into one or more `AnalyticsQuery`s against the Cube\n * runtime, then post-processes the results:\n * - resolves the base measures a selection needs (including derived deps),\n * - applies measure-scoped filters via supplementary grouped queries,\n * - evaluates derived measures (ratio/sum/difference/product) row-by-row (Q1),\n * - shifts the query for `compareTo` (previousPeriod / previousYear) and\n * attaches `<measure>__compare` columns,\n * - computes server-side totals (`selection.totals.groupings`, #1753) by\n * re-running the selection per dimension subset, so matrix subtotals and\n * the grand total use each measure's true aggregate,\n * - orders and windows the final grid (`order` / `limit` / `offset`, #3588).\n *\n * **Where ordering happens, and why here.** `order`/`limit`/`offset` are applied\n * to the ASSEMBLED grid — after measure-scoped sub-queries are merged in, after\n * `compareTo` columns are attached, and after derived measures are computed —\n * never by forwarding them blindly to every sub-query. Two reasons:\n *\n * 1. **Correctness.** A supplementary measure-scoped query selects ONE measure;\n * forwarding `ORDER BY <other_measure>` to it emits SQL referencing a column\n * that query never selects, and forwarding `LIMIT` truncates it before the\n * merge, so rows silently vanish from the grid. A derived measure has no SQL\n * column at all, yet is a perfectly reasonable sort key.\n * 2. **Coverage.** Only `NativeSQLStrategy` honours `order`/`limit`; the\n * ObjectQL aggregate path has nowhere to put them (`EngineAggregateOptions`\n * has no ordering grammar), and date-bucketed queries are *forced* down that\n * path because native SQL declines granularity. Sorting here makes ordering\n * work identically on every driver and strategy.\n *\n * The single-query case still pushes `order`/`limit`/`offset` DOWN into the SQL\n * (see `canPushDownWindow`) so the database does the work and the echoed `sql`\n * shows it; the post-pass is then a no-op re-sort of already-sorted rows.\n *\n * **What the sort key IS for a label-bearing dimension (#3680).** An order key\n * naming a `select` or `lookup`/`master_detail` dimension sorts by the DISPLAY\n * label the response will carry (option label / related record name), not the\n * stored value — a \"sort by Account\" ordered by opaque FK ids presents as\n * arbitrary once the labels render. The mapping comes through an injected\n * {@link OrderLabelResolver} (built by `queryDataset` over the same\n * label-resolution capabilities the display pass uses); rows keep their raw\n * values — only the COMPARISON substitutes the label — so drill metadata still\n * snapshots stored values downstream. Such keys are never pushed into SQL (the\n * label is not a column there), and the label fetch happens BEFORE `applyWindow`\n * so a \"top 10 by account name\" truncates the right ten.\n *\n * RLS/tenant scoping is NOT handled here — it is enforced inside the strategy\n * via the StrategyContext read-scope hook (D-C). This layer is pure query\n * shaping + arithmetic; the order-label hook is an injected interface, not an\n * engine dependency.\n */\n\n/**\n * Expand `{filter-placeholder}` values across everything a dataset query\n * compares on (framework#3582): the dataset's intrinsic `filter`, the\n * presentation's `runtimeFilter` (a dashboard widget's own scope), every\n * measure-scoped filter, and the `dateRange` bounds of the selection's time\n * dimensions.\n *\n * The dashboard path needs its own call rather than inheriting the ObjectQL\n * engine's: `NativeSQLStrategy` compiles a raw `SELECT … WHERE` and binds\n * comparands directly, so a widget filtered on `{current_year_start}` never\n * passes through `engine.find()` at all — which is exactly why the token\n * reached SQLite as the literal text and every such widget rendered zero.\n *\n * Inputs are treated as immutable: a `CompiledDataset` lives in the service's\n * registry across requests, so resolving in place would bake one request's\n * user id (and one day's dates) into every later render. New objects are\n * allocated only when the tree actually held a placeholder.\n */\nfunction resolveSelectionTokens(\n compiled: CompiledDataset,\n selection: DatasetSelection,\n context?: ExecutionContext,\n): { compiled: CompiledDataset; selection: DatasetSelection } {\n // One instant for the whole call: the intrinsic filter, the runtime filter\n // and each measure filter are resolved in separate passes, and a query whose\n // pieces disagreed about \"now\" could straddle a period boundary — the primary\n // grid scoped to this month while a measure-scoped sub-query saw the next.\n const tokenCtx = filterTokenContextFrom(context, new Date());\n const resolve = <T>(v: T): T => resolveFilterTokens(v, tokenCtx);\n\n const filter = resolve(compiled.filter);\n const measureFilters = resolve(compiled.measureFilters);\n const runtimeFilter = resolve(selection.runtimeFilter);\n const timeDimensions = selection.timeDimensions?.map((td) =>\n td.dateRange == null ? td : { ...td, dateRange: resolve(td.dateRange) },\n );\n\n const compiledChanged =\n filter !== compiled.filter || measureFilters !== compiled.measureFilters;\n const selectionChanged =\n runtimeFilter !== selection.runtimeFilter ||\n (timeDimensions !== undefined &&\n timeDimensions.some((td, i) => td !== selection.timeDimensions![i]));\n\n return {\n compiled: compiledChanged ? { ...compiled, filter, measureFilters } : compiled,\n selection: selectionChanged ? { ...selection, runtimeFilter, timeDimensions } : selection,\n };\n}\n\n/** AND two optional FilterConditions into one (MongoDB-style). */\nexport function combineFilters(\n a?: FilterCondition,\n b?: FilterCondition,\n): FilterCondition | undefined {\n if (a && b) return { $and: [a, b] } as FilterCondition;\n return a ?? b;\n}\n\n/**\n * Evaluate derived measures on each aggregated row, mutating a shallow copy.\n * Division by zero (and missing operands) yields `null` rather than Infinity/NaN.\n */\nexport function evaluateDerivedMeasures(\n rows: Record<string, unknown>[],\n derived: DerivedMeasureSpec[],\n): Record<string, unknown>[] {\n if (derived.length === 0) return rows;\n return rows.map((row) => {\n const out = { ...row };\n for (const d of derived) {\n out[d.name] = computeDerived(d, out);\n }\n return out;\n });\n}\n\nfunction num(v: unknown): number | null {\n if (v == null) return null;\n const n = typeof v === 'number' ? v : Number(v);\n return Number.isFinite(n) ? n : null;\n}\n\nfunction computeDerived(d: DerivedMeasureSpec, row: Record<string, unknown>): number | null {\n const vals = d.of.map((name) => num(row[name]));\n if (vals.some((v) => v === null)) return null;\n const nums = vals as number[];\n switch (d.op) {\n case 'ratio': {\n if (nums.length < 2 || nums[1] === 0) return null;\n return nums[0] / nums[1];\n }\n case 'difference':\n return nums.slice(1).reduce((acc, v) => acc - v, nums[0]);\n case 'sum':\n return nums.reduce((acc, v) => acc + v, 0);\n case 'product':\n return nums.reduce((acc, v) => acc * v, 1);\n default:\n return null;\n }\n}\n\n// ── date bucketing (#3588) ───────────────────────────────────────────────────\n\n/** The date-bucket vocabulary shared by the dataset, the selection, and the\n * bucketing utilities in `@objectstack/core`. */\nexport type DateGranularityValue = NonNullable<DatasetSelection['dateGranularity']>;\n\n/**\n * The EFFECTIVE bucket size for one date dimension of a selection — the single\n * source of truth for granularity precedence.\n *\n * Precedence, per dimension:\n * 1. a `granularity` already stated on that dimension's `timeDimensions`\n * entry — never overridden;\n * 2. `selection.dateGranularity` — the presentation's choice, so a widget can\n * bucket by month without the dataset committing every consumer to it;\n * 3. `datasetDefault` — the dataset dimension's own `dateGranularity`.\n *\n * The unit of precedence is the GRANULARITY, not the entry: a `timeDimensions`\n * entry carrying only a `dateRange` (what `compareTo` needs) states a WINDOW,\n * not a bucket size, and must not suppress bucketing.\n *\n * **Why this is exported.** The bucket size chosen here decides three things\n * that MUST agree: the `GROUP BY` the query compiles to, the humanized label\n * each bucket key is rendered as, and the half-open `[gte, lt)` range a bucket\n * drills into. When the query layer resolved granularity and the post-processing\n * in `analytics-service` read the dataset default instead, they silently\n * disagreed for every selection that overrode it — a `year` query came back\n * labelled `1970-01` (a year bucket re-formatted as a month), a `day` query\n * collapsed to duplicate month labels, and `quarter`/`year` lost their drill\n * ranges entirely. One function, called from all three sites, is what stops\n * that drift recurring.\n */\nexport function resolveDimensionGranularity(\n selection: Pick<DatasetSelection, 'timeDimensions' | 'dateGranularity'>,\n dimension: string,\n datasetDefault?: string,\n): DateGranularityValue | undefined {\n // `timeDimensions[].granularity` and the compiled cube's `granularities` are\n // both typed as bare strings by their own layers (Cube.js heritage), but the\n // only values that reach here come from the dataset/selection granularity\n // vocabulary — the same five the bucketing utilities accept.\n const stated = (selection.timeDimensions ?? []).find((t) => t.dimension === dimension)?.granularity;\n if (stated) return stated as DateGranularityValue;\n return selection.dateGranularity ?? (datasetDefault as DateGranularityValue | undefined);\n}\n\n// ── ordering + windowing (#3588) ─────────────────────────────────────────────\n\n/**\n * Compare two grouped-cell values for ORDER BY, ascending.\n *\n * Nulls sort LAST regardless of direction (the SQL `NULLS LAST` convention, and\n * the one users expect: an empty bucket shouldn't win a \"top 10 by revenue\").\n * The caller negates the result for `desc`, so the null branch deliberately\n * returns its verdict BEFORE that negation can flip it — see `compareRows`.\n *\n * Numbers (and numeric strings, which is how some drivers return SUM results)\n * compare numerically so 9 sorts below 10; everything else compares as a string.\n * Dates arrive here already bucketed to sort-stable keys (\"2026-04\", \"2026-Q2\"),\n * so lexicographic ordering is chronological for them too.\n */\nfunction compareValues(a: unknown, b: unknown): number {\n const aNull = a == null || a === '';\n const bNull = b == null || b === '';\n if (aNull || bNull) return aNull && bNull ? 0 : aNull ? 1 : -1;\n if (a instanceof Date || b instanceof Date) {\n return Number(a instanceof Date ? a.getTime() : a) - Number(b instanceof Date ? b.getTime() : b);\n }\n if (typeof a === 'boolean' || typeof b === 'boolean') {\n return Number(a) - Number(b);\n }\n const an = typeof a === 'number' ? a : Number(a);\n const bn = typeof b === 'number' ? b : Number(b);\n if (Number.isFinite(an) && Number.isFinite(bn)) return an - bn;\n return String(a).localeCompare(String(b));\n}\n\n/**\n * Order rows by each key in `order`, in the object's own key order (first key is\n * the primary sort). Returns a NEW array; the input is not mutated. Null/empty\n * cells stay last in both directions (see {@link compareValues}).\n *\n * `sortKeys` substitutes the COMPARED value per key (#3680): when it holds a map\n * for an order key, each cell compares by its mapped value — the display label a\n * label-bearing dimension will render as — falling back to the raw cell where\n * unmapped (an orphaned id or RLS-hidden record renders raw too, so sort and\n * display stay consistent). The rows themselves are never rewritten here.\n */\nexport function applyOrdering(\n rows: Record<string, unknown>[],\n order: Record<string, 'asc' | 'desc'> | undefined,\n sortKeys?: Record<string, Map<unknown, unknown>>,\n): Record<string, unknown>[] {\n const keys = Object.entries(order ?? {});\n if (keys.length === 0 || rows.length < 2) return rows;\n // Array.prototype.sort is stable (ES2019+), so equal rows keep the order the\n // grouping produced — an important property for reproducible LIMITs.\n return [...rows].sort((ra, rb) => {\n for (const [key, dir] of keys) {\n const map = sortKeys?.[key];\n const av = map?.get(ra[key]) ?? ra[key];\n const bv = map?.get(rb[key]) ?? rb[key];\n const aNull = av == null || av === '';\n const bNull = bv == null || bv === '';\n // Nulls last in BOTH directions — decided before `desc` negation.\n if (aNull || bNull) {\n if (aNull && bNull) continue;\n return aNull ? 1 : -1;\n }\n const c = compareValues(av, bv);\n if (c !== 0) return dir === 'desc' ? -c : c;\n }\n return 0;\n });\n}\n\n/** Apply `offset`/`limit` to an already-ordered grid. */\nexport function applyWindow(\n rows: Record<string, unknown>[],\n limit?: number,\n offset?: number,\n): Record<string, unknown>[] {\n const start = offset != null && offset > 0 ? offset : 0;\n if (start === 0 && limit == null) return rows;\n return rows.slice(start, limit != null ? start + limit : undefined);\n}\n\n/**\n * Validate `order` keys and resolve the EFFECTIVE ordering for a selection.\n *\n * A key must name something the caller actually selected — a dimension, a\n * measure, or a `<measure>__compare` column. An unknown key throws rather than\n * being dropped: silently ignoring `sortBy` is precisely the failure mode this\n * change exists to remove (#3588), and a mistyped sort key that quietly returns\n * arbitrarily-ordered rows is worse than a loud 400.\n *\n * When `limit`/`offset` is requested WITHOUT an order, the selected dimensions\n * ascending become the implicit ordering, so the truncated window is\n * reproducible instead of \"whatever the group-by happened to emit\".\n */\nexport function resolveOrdering(\n selection: DatasetSelection,\n dimensions: string[],\n): Record<string, 'asc' | 'desc'> | undefined {\n const order = selection.order;\n if (order && Object.keys(order).length > 0) {\n const selectable = new Set<string>([\n ...dimensions,\n ...selection.measures,\n ...selection.measures.map((m) => `${m}__compare`),\n ]);\n const unknown = Object.keys(order).filter((k) => !selectable.has(k));\n if (unknown.length) {\n throw new Error(\n `[dataset-executor] order key(s) ${unknown.map((k) => `\"${k}\"`).join(', ')} — ` +\n `not a selected dimension or measure. Selectable here: ` +\n `${[...selectable].join(', ') || '(none)'}.`,\n );\n }\n return order;\n }\n // Implicit, deterministic ordering so a bare `limit` is reproducible.\n if ((selection.limit != null || selection.offset != null) && dimensions.length > 0) {\n return Object.fromEntries(dimensions.map((d) => [d, 'asc' as const]));\n }\n return undefined;\n}\n\n// ── compareTo date math (deterministic — no Date.now) ────────────────────────\n\nfunction parseUTC(date: string): number {\n // Accepts 'YYYY-MM-DD' (and ISO datetimes); interpreted as UTC.\n const ms = Date.parse(date.length === 10 ? `${date}T00:00:00Z` : date);\n if (Number.isNaN(ms)) throw new Error(`[dataset-executor] invalid date in dateRange: \"${date}\"`);\n return ms;\n}\n\nconst DAY_MS = 86_400_000;\n\nfunction toISODate(ms: number): string {\n return new Date(ms).toISOString().slice(0, 10);\n}\n\nfunction shiftYear(date: string, years: number): string {\n const d = new Date(parseUTC(date));\n d.setUTCFullYear(d.getUTCFullYear() + years);\n return toISODate(d.getTime());\n}\n\n/** Compute the comparison window for a [start,end] range. */\nexport function shiftRange(range: [string, string], kind: CompareTo['kind']): [string, string] {\n const [start, end] = range;\n if (kind === 'previousYear') {\n return [shiftYear(start, -1), shiftYear(end, -1)];\n }\n // previousPeriod — the equal-length window ending the day before `start`.\n const startMs = parseUTC(start);\n const endMs = parseUTC(end);\n const lengthDays = Math.round((endMs - startMs) / DAY_MS) + 1;\n const prevEndMs = startMs - DAY_MS;\n const prevStartMs = prevEndMs - (lengthDays - 1) * DAY_MS;\n return [toISODate(prevStartMs), toISODate(prevEndMs)];\n}\n\nexport class DatasetExecutor {\n /**\n * @param service - The analytics service the executor issues its queries to.\n * @param orderLabels - Optional sort-key label hook (#3680). When provided,\n * an order key naming a label-bearing (`select`/`lookup`) dimension sorts\n * by its display label instead of the stored value. Omit to sort by stored\n * values everywhere (e.g. the draft-preview path, whose seed rows already\n * carry display names).\n */\n constructor(\n private readonly service: IAnalyticsService,\n private readonly orderLabels?: OrderLabelResolver,\n ) {}\n\n /**\n * Execute a dataset selection and return the shaped rows (+ field metadata).\n *\n * @param context - The request's ExecutionContext, threaded into every\n * underlying `IAnalyticsService.query` so the tenant/RLS read scope is\n * applied per request (ADR-0021 D-C).\n */\n async execute(\n compiledInput: CompiledDataset,\n selectionInput: DatasetSelection,\n context?: ExecutionContext,\n ): Promise<AnalyticsResult> {\n // framework#3582 — expand `{current_quarter_start}` / `{current_user_id}`\n // placeholders BEFORE any query is shaped, once for the whole call so every\n // sub-query (measure-scoped, totals, compareTo) shares one instant.\n const { compiled, selection } = resolveSelectionTokens(compiledInput, selectionInput, context);\n\n const result = await this.executeSelection(compiled, selection, context);\n\n // Server-side totals (#1753) — re-run the selection grouped by each\n // requested dimension subset, so a subtotal/grand total is the measure's\n // TRUE aggregate over the underlying rows (an avg total is the average of\n // all rows, not of bucket averages). Re-running the full pipeline keeps\n // measure-scoped filters, derived measures, and compareTo consistent with\n // the primary grid. order/limit/offset are dropped: totals cover the whole\n // selection, and an order key may reference a dimension the grouping drops.\n const groupings = selection.totals?.groupings;\n if (groupings?.length) {\n const selected = new Set(selection.dimensions ?? []);\n const totals: NonNullable<AnalyticsResult['totals']> = [];\n for (const grouping of groupings) {\n const unknown = grouping.filter((d) => !selected.has(d));\n if (unknown.length) {\n throw new Error(\n `[dataset-executor] totals grouping [${grouping.join(', ')}] is not a subset of the selected dimensions — unknown: ${unknown.join(', ')}.`,\n );\n }\n const sub = await this.executeSelection(compiled, {\n ...selection,\n dimensions: grouping,\n totals: undefined,\n order: undefined,\n limit: undefined,\n offset: undefined,\n }, context);\n totals.push({ dimensions: grouping, rows: sub.rows });\n }\n result.totals = totals;\n }\n\n return result;\n }\n\n private async executeSelection(\n compiled: CompiledDataset,\n selection: DatasetSelection,\n context?: ExecutionContext,\n ): Promise<AnalyticsResult> {\n const derivedByName = new Map(compiled.derived.map((d) => [d.name, d]));\n const selectedDerived = selection.measures\n .map((m) => derivedByName.get(m))\n .filter((d): d is DerivedMeasureSpec => !!d);\n\n // Base measures = selected non-derived + dependencies of selected derived.\n const baseMeasures = new Set<string>();\n for (const m of selection.measures) {\n if (!derivedByName.has(m)) baseMeasures.add(m);\n }\n for (const d of selectedDerived) {\n for (const dep of d.of) baseMeasures.add(dep);\n }\n\n // Split measures into those with a scoped filter and those without.\n const unfiltered: string[] = [];\n const filtered: string[] = [];\n for (const m of baseMeasures) {\n (compiled.measureFilters[m] ? filtered : unfiltered).push(m);\n }\n\n const baseFilter = combineFilters(compiled.filter, selection.runtimeFilter);\n const dimensions = selection.dimensions ?? [];\n\n // Effective ordering — validated against what this selection projects, with\n // a deterministic dimension order synthesized for a bare `limit` (#3588).\n const order = resolveOrdering(selection, dimensions);\n\n // #3680 — order keys naming a select/lookup dimension sort by the DISPLAY\n // label the response will carry, not the stored value / FK id. Resolved\n // over the assembled grid below; identified up front because such a key\n // also disqualifies SQL pushdown (the label is not a column the database\n // could ORDER BY, and a SQL LIMIT would truncate the wrong window).\n const labelOrderKeys = this.orderLabels\n ? Object.keys(order ?? {}).filter(\n (k) => dimensions.includes(k) && this.orderLabels!.isLabelBearing(k),\n )\n : [];\n\n // Push `order`/`limit`/`offset` down into the SQL only when this selection\n // is ONE query whose columns can satisfy them. With supplementary\n // measure-scoped queries, a compareTo pass, or derived measures in play, the\n // grid is assembled from several results — a sub-query LIMIT would drop rows\n // before the merge and an ORDER BY would name a column that sub-query never\n // selects. Those cases order in memory below instead.\n const singleQuery = filtered.length === 0 && !selection.compareTo && selectedDerived.length === 0;\n const pushDownKeys = new Set<string>([...dimensions, ...unfiltered]);\n const canPushDownWindow =\n singleQuery && labelOrderKeys.length === 0 &&\n Object.keys(order ?? {}).every((k) => pushDownKeys.has(k));\n const windowQuery = canPushDownWindow\n ? { order, limit: selection.limit, offset: selection.offset }\n : undefined;\n\n // Primary query: all unfiltered base measures in one pass. When every base\n // measure is filter-scoped, the supplementary queries below build the grid.\n let result: AnalyticsResult;\n if (unfiltered.length > 0 || filtered.length === 0) {\n result = await this.service.query(this.buildQuery(compiled, {\n measures: unfiltered,\n dimensions,\n where: baseFilter,\n selection,\n contextTimezone: context?.timezone,\n window: windowQuery,\n }), context);\n } else {\n result = { rows: [], fields: [] };\n }\n\n // Supplementary queries: one per measure-scoped filter, merged by dimension key.\n for (const m of filtered) {\n const mFilter = combineFilters(baseFilter, compiled.measureFilters[m]);\n const sub = await this.service.query(this.buildQuery(compiled, {\n measures: [m], dimensions, where: mFilter, selection,\n contextTimezone: context?.timezone,\n }), context);\n result.rows = mergeByDimensions(result.rows, sub.rows, dimensions, [m]);\n result.fields.push({ name: m, type: 'number' });\n }\n\n // compareTo — run a shifted query over the same base measures and attach.\n if (selection.compareTo) {\n const compareRows = await this.runCompare(compiled, selection, [...baseMeasures], dimensions, baseFilter, context);\n result.rows = mergeByDimensions(\n result.rows,\n compareRows,\n dimensions,\n [...baseMeasures].map((m) => `${m}__compare`),\n );\n for (const m of baseMeasures) result.fields.push({ name: `${m}__compare`, type: 'number' });\n }\n\n // Derived measures (computed from base + compare columns already present).\n result.rows = evaluateDerivedMeasures(result.rows, selectedDerived);\n for (const d of selectedDerived) result.fields.push({ name: d.name, type: 'number' });\n\n // Order + window the assembled grid (#3588). Every column the caller may\n // sort by exists by now — merged measure-scoped values, `__compare`\n // columns, and derived measures included. When the window was already\n // pushed into SQL this re-sorts an already-sorted grid (a no-op) and\n // re-slices an already-sliced one; when it could not be (the ObjectQL\n // aggregate path has no ordering grammar, and date-bucketed queries are\n // forced down it), this is what makes `sortBy` work at all.\n //\n // #3680 — for label-bearing order keys, substitute the display label as\n // the SORT KEY, resolved over the grid's distinct values BEFORE the window\n // (a \"top 10 by account name\" must pick the ten by name). Rows keep their\n // raw values — display rewriting stays in `queryDataset`, after the drill\n // metadata snapshots the stored values. A select dimension resolves from\n // field metadata (no query); a lookup costs one batched id→name read.\n let sortKeys: Record<string, Map<unknown, unknown>> | undefined;\n for (const key of labelOrderKeys) {\n const values = [...new Set(result.rows.map((r) => r[key]).filter((v) => v != null))];\n if (values.length === 0) continue;\n const labels = await this.orderLabels!.resolveLabels(key, values);\n if (labels && labels.size > 0) (sortKeys ??= {})[key] = labels;\n }\n result.rows = applyOrdering(result.rows, order, sortKeys);\n result.rows = applyWindow(result.rows, selection.limit, selection.offset);\n\n return result;\n }\n\n private buildQuery(\n compiled: CompiledDataset,\n opts: {\n measures: string[];\n dimensions: string[];\n where?: FilterCondition;\n selection: DatasetSelection;\n contextTimezone?: string;\n /**\n * Ordering/window to push DOWN into this query. Set only for a selection\n * the caller proved is a single self-sufficient query (see\n * `canPushDownWindow`); omitted for supplementary/compare sub-queries,\n * which must return their full grid for the merge.\n */\n window?: { order?: Record<string, 'asc' | 'desc'>; limit?: number; offset?: number };\n },\n ): AnalyticsQuery {\n const q: AnalyticsQuery = {\n cube: compiled.cube.name,\n measures: opts.measures,\n dimensions: opts.dimensions,\n // Precedence: explicit selection tz → request's reference tz\n // (ExecutionContext.timezone, ADR-0053 Phase 2) → UTC.\n timezone: opts.selection.timezone ?? opts.contextTimezone ?? 'UTC',\n };\n if (opts.where) q.where = opts.where as Record<string, unknown>;\n // Bucket selected date dimensions. Without this a date dimension groups by\n // the raw timestamp — one bucket per ROW, which is why a \"new accounts by\n // month\" bar chart drew one bar per account instead of one per month\n // (#3588).\n //\n // Granularity precedence, per dimension:\n // 1. a `granularity` already stated on that dimension's\n // `selection.timeDimensions` entry — never overridden;\n // 2. `selection.dateGranularity` — the PRESENTATION's choice, so a widget\n // can bucket by month without the dataset committing every consumer to\n // that granularity;\n // 3. the dataset dimension's own default (the compiler lowers an explicit\n // `dateGranularity` to a single-entry `granularities`; the 5-entry\n // \"all granularities\" list means the dataset stated no default).\n //\n // Note the unit of precedence is the GRANULARITY, not the entry. A\n // `timeDimensions` entry that only carries a `dateRange` (which is exactly\n // what `compareTo` needs) states a WINDOW, not a bucket size — letting its\n // mere presence suppress bucketing left the compared pass grouping raw\n // timestamps while the primary pass grouped months, so the two grids shared\n // no dimension key and every `__compare` column came back empty.\n const selTimeDims = opts.selection.timeDimensions ?? [];\n const selDims = new Set(selTimeDims.map((t) => t.dimension));\n const granularityFor = (name: string): string | undefined => {\n const cd = compiled.cube.dimensions[name];\n if (cd?.type !== 'time') return undefined;\n const datasetDefault = cd.granularities?.length === 1 ? String(cd.granularities[0]) : undefined;\n return resolveDimensionGranularity(opts.selection, name, datasetDefault);\n };\n // Fill in a bucket size for caller-supplied entries that named none.\n const resolvedTimeDims = selTimeDims.map((t) => {\n if (t.granularity) return t;\n const granularity = granularityFor(t.dimension);\n return granularity ? { ...t, granularity } : t;\n });\n const explicitTimeDims: Array<{ dimension: string; granularity: string }> = [];\n for (const name of opts.dimensions) {\n if (selDims.has(name)) continue;\n const granularity = granularityFor(name);\n if (granularity) explicitTimeDims.push({ dimension: name, granularity });\n }\n const mergedTimeDims = [...resolvedTimeDims, ...explicitTimeDims];\n if (mergedTimeDims.length > 0) q.timeDimensions = mergedTimeDims as AnalyticsQuery['timeDimensions'];\n // Ordering/window: pushed down ONLY when the caller vouched for it. The\n // executor always re-applies both over the assembled grid, so omitting them\n // here costs correctness nothing — it only moves the work to memory.\n if (opts.window?.order && Object.keys(opts.window.order).length > 0) q.order = opts.window.order;\n if (opts.window?.limit != null) q.limit = opts.window.limit;\n if (opts.window?.offset != null) q.offset = opts.window.offset;\n return q;\n }\n\n private async runCompare(\n compiled: CompiledDataset,\n selection: DatasetSelection,\n measures: string[],\n dimensions: string[],\n baseFilter: FilterCondition | undefined,\n context?: ExecutionContext,\n ): Promise<Record<string, unknown>[]> {\n const cmp = selection.compareTo!;\n const td = (selection.timeDimensions ?? []).find((t) => t.dimension === cmp.dimension);\n if (!td || !td.dateRange) {\n throw new Error(\n `[dataset-executor] compareTo requires a timeDimension \"${cmp.dimension}\" with a dateRange.`,\n );\n }\n const range: [string, string] = Array.isArray(td.dateRange)\n ? [td.dateRange[0], td.dateRange[1] ?? td.dateRange[0]]\n : [td.dateRange, td.dateRange];\n const shifted = shiftRange(range, cmp.kind);\n const shiftedTd = (selection.timeDimensions ?? []).map((t) =>\n t.dimension === cmp.dimension ? { ...t, dateRange: shifted } : t,\n );\n // Built through `buildQuery` so the comparison pass buckets its date\n // dimensions EXACTLY like the primary pass. Hand-rolling the query here\n // skipped granularity resolution, so a bucketed primary grid (\"2026-04\")\n // was merged against raw-timestamp comparison rows and no dimension key\n // ever matched — every `__compare` column came back empty. The shifted\n // `timeDimensions` still win for their own dimension (rule 1 of the\n // precedence chain); `window` is deliberately omitted — the comparison grid\n // must stay whole for the merge.\n const sub = await this.service.query(this.buildQuery(compiled, {\n measures,\n dimensions,\n where: baseFilter,\n selection: { ...selection, timeDimensions: shiftedTd },\n contextTimezone: context?.timezone,\n }), context);\n // Rename measure columns to `<measure>__compare` so they merge alongside primary.\n return sub.rows.map((row) => {\n const out: Record<string, unknown> = {};\n for (const dim of dimensions) out[dim] = row[dim];\n for (const m of measures) out[`${m}__compare`] = row[m];\n return out;\n });\n }\n}\n\n/**\n * Left-merge `extra` rows onto `base` rows by their dimension-key tuple,\n * copying the listed value columns. Rows in `extra` with no base match are\n * appended (outer-ish merge so comparison-only buckets still surface).\n */\nexport function mergeByDimensions(\n base: Record<string, unknown>[],\n extra: Record<string, unknown>[],\n dimensions: string[],\n valueColumns: string[],\n): Record<string, unknown>[] {\n const keyOf = (row: Record<string, unknown>) => dimensions.map((d) => String(row[d] ?? '')).join('\u0001');\n const index = new Map<string, Record<string, unknown>>();\n for (const row of base) index.set(keyOf(row), row);\n\n for (const row of extra) {\n const key = keyOf(row);\n const target = index.get(key);\n if (target) {\n for (const c of valueColumns) target[c] = row[c];\n } else {\n const fresh: Record<string, unknown> = {};\n for (const d of dimensions) fresh[d] = row[d];\n for (const c of valueColumns) fresh[c] = row[c];\n index.set(key, fresh);\n base.push(fresh);\n }\n }\n return base;\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Dimension display-label resolution (ADR-0021).\n *\n * Analytics groups by the raw stored value of a dimension field. For two field\n * kinds that value is NOT human-readable:\n *\n * - **select** — grouped by the stored option `value` (e.g. `backlog`), but the\n * user-facing text is the option `label` (e.g. `Backlog`).\n * - **lookup / master_detail** — grouped by the foreign-key `id` (e.g.\n * `8eqtuKI4G9IhUsPS`), but the user-facing text is the related record's\n * display field (its name/title).\n *\n * `resolveDimensionLabels` post-processes the result rows IN PLACE, replacing the\n * raw value at `row[dimension.name]` with its display label when one is found.\n * Unresolved values are left untouched so an orphaned id still renders as itself\n * rather than blanking out. Date / number / plain-string dimensions are no-ops.\n *\n * The resolution LOGIC lives here (and is unit-tested); the low-level capabilities\n * — reading an object's field map and fetching id→label pairs — are injected via\n * {@link DimensionLabelDeps} so this module stays free of any engine dependency.\n */\n\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\n\n/** The minimal field shape this resolver needs. */\nexport interface FieldMetaLite {\n type?: string;\n /** Lookup / master_detail target object name. */\n reference?: string;\n /** Select options — the value→label source. */\n options?: Array<{ value: unknown; label?: string }>;\n}\n\n/** Capabilities the resolver needs from the runtime (injected by the plugin). */\nexport interface DimensionLabelDeps {\n /** Return the field map for an object, or `undefined` if unknown. */\n getObjectFields(objectName: string): Record<string, FieldMetaLite> | undefined;\n /**\n * Fetch a map of `id → display label` for the given ids of a target object.\n * The implementation chooses the target's display field. Returning an empty\n * map (e.g. no display field, no data access) leaves the ids unresolved.\n *\n * `scope` (ADR-0021 D-C, #3602) is the TARGET object's own read scope — the\n * RLS/tenant `FilterCondition` the implementation must AND into the label\n * lookup so this never reveals a related record the target object's RLS would\n * hide. The label lookup is a per-record read (`group by id`) dressed as an\n * aggregate; without the scope it leaks display names whenever the referenced\n * object is more restricted than the base object whose rows carry the id.\n * `undefined` means \"no scope for this object\" (global table / unrestricted\n * caller) — the same contract as the read-scope provider.\n *\n * `context` is the request's ExecutionContext — the SECOND belt on the same\n * read (#3602). `scope` is the analytics layer's own predicate; forwarding the\n * context lets the ENGINE's middleware chain scope this per-record read\n * itself, so it stays scoped even if a caller ever reaches this hook without\n * a resolved `scope`. Implementations bridging to an ObjectQL engine MUST\n * forward it; a bridge with nowhere to put it may ignore it.\n */\n fetchRecordLabels(\n targetObject: string,\n ids: unknown[],\n scope?: Record<string, unknown>,\n context?: ExecutionContext,\n ): Promise<Map<unknown, string>>;\n}\n\n/**\n * Resolve the TARGET object's read scope for a label lookup (#3602). Returns the\n * object's RLS/tenant `FilterCondition`, `null`/`undefined` when the object is\n * unscoped, or a rejected promise when the scope cannot be resolved — in which\n * case the resolver fails CLOSED (skips that dimension's labels) rather than\n * fetching unscoped names.\n */\nexport type LabelScopeResolver = (\n targetObject: string,\n) => Promise<Record<string, unknown> | null | undefined> | Record<string, unknown> | null | undefined;\n\nconst LOOKUP_TYPES = new Set(['lookup', 'master_detail']);\n\n/**\n * Sort-key label resolution for `DatasetSelection.order` (#3680).\n *\n * The executor sorts the assembled grid BEFORE `queryDataset` rewrites stored\n * dimension values into display labels, so an order key naming a `select` or\n * `lookup`/`master_detail` dimension used to sort by the stored value / FK id —\n * an order that presents as arbitrary once the labels render. This hook hands\n * the executor JUST the value→label mapping for such a dimension so it can sort\n * by what the user will actually read, while the rows keep their raw values\n * (drill metadata depends on them) and ordering + windowing stay one adjacent\n * step. The executor stays engine-free: it sees this interface, never the\n * engine behind it.\n */\nexport interface OrderLabelResolver {\n /**\n * Whether the dimension's stored value differs from the label it renders as\n * (`select` options, `lookup`/`master_detail` FK ids). Synchronous — the\n * executor consults it when deciding whether the window may be pushed into\n * SQL, before any query runs.\n */\n isLabelBearing(dimension: string): boolean;\n /**\n * Map the given raw stored values of one dimension to display labels.\n * Values missing from the map sort by their raw form — the same thing the\n * user will see rendered for them.\n */\n resolveLabels(dimension: string, values: unknown[]): Promise<Map<unknown, string> | undefined>;\n}\n\n/**\n * Build the executor's {@link OrderLabelResolver} from the dataset's dimension\n * list and the injected label capabilities. Mirrors the classification in\n * {@link resolveDimensionLabels}: a dimension is label-bearing when its field\n * carries select `options` or is a lookup/master_detail with a `reference`.\n *\n * - `select` resolves from field metadata — no query at all.\n * - `lookup`/`master_detail` costs ONE batched id→name read over the distinct\n * grouped values, scoped to the REFERENCED object's own RLS (#3602). Fail\n * closed: an unresolvable scope degrades to sorting by the stored id rather\n * than fetching unscoped — consistent with the display pass, which renders\n * the raw id in that case too.\n */\nexport function createOrderLabelResolver(\n baseObject: string,\n dims: Array<{ name: string; field: string }>,\n deps: DimensionLabelDeps,\n resolveScope?: LabelScopeResolver,\n context?: ExecutionContext,\n): OrderLabelResolver {\n const dimByName = new Map(dims.map((d) => [d.name, d]));\n const metaFor = (dimension: string): FieldMetaLite | undefined => {\n const dim = dimByName.get(dimension);\n return dim ? deps.getObjectFields(baseObject)?.[dim.field] : undefined;\n };\n return {\n isLabelBearing(dimension) {\n const meta = metaFor(dimension);\n if (!meta) return false;\n if (Array.isArray(meta.options) && meta.options.length > 0) return true;\n return !!(meta.type && LOOKUP_TYPES.has(meta.type) && meta.reference);\n },\n async resolveLabels(dimension, values) {\n const meta = metaFor(dimension);\n if (!meta) return undefined;\n if (Array.isArray(meta.options) && meta.options.length > 0) {\n const labelByValue = new Map<unknown, string>();\n for (const opt of meta.options) {\n if (opt && opt.label != null) labelByValue.set(opt.value, String(opt.label));\n }\n return labelByValue;\n }\n if (meta.type && LOOKUP_TYPES.has(meta.type) && meta.reference) {\n let scope: Record<string, unknown> | null | undefined;\n if (resolveScope) {\n try {\n scope = await resolveScope(meta.reference);\n } catch {\n return undefined;\n }\n }\n return deps.fetchRecordLabels(meta.reference, values, scope ?? undefined, context);\n }\n return undefined;\n },\n };\n}\n\n/**\n * Wrap a {@link DimensionLabelDeps} so repeated `fetchRecordLabels` calls\n * within ONE request fetch each id at most once. A selection that sorts by a\n * lookup dimension resolves labels twice — once PRE-window for the sort keys\n * (#3680, over the full grid's ids), once post-window for display (a subset of\n * the same ids) — so with this cache the display pass costs no extra query.\n *\n * Per-request only: entries are keyed by target object alone, which is safe\n * because an object's read scope is constant within one request. Never share\n * an instance across requests.\n */\nexport function withLabelFetchCache(deps: DimensionLabelDeps): DimensionLabelDeps {\n // Per target object: id → label, with `null` marking \"fetched, no label\"\n // (RLS-hidden or orphaned) so unresolvable ids are not re-fetched every call.\n const cache = new Map<string, Map<unknown, string | null>>();\n return {\n getObjectFields: (objectName) => deps.getObjectFields(objectName),\n async fetchRecordLabels(targetObject, ids, scope, context) {\n let known = cache.get(targetObject);\n if (!known) {\n known = new Map();\n cache.set(targetObject, known);\n }\n const missing = ids.filter((id) => !known.has(id));\n if (missing.length > 0) {\n const fetched = await deps.fetchRecordLabels(targetObject, missing, scope, context);\n for (const id of missing) known.set(id, fetched.get(id) ?? null);\n }\n const out = new Map<unknown, string>();\n for (const id of ids) {\n const label = known.get(id);\n if (label != null) out.set(id, label);\n }\n return out;\n },\n };\n}\n\n/** Date-dimension granularity (mirrors the dataset `dateGranularity` enum). */\nexport type DateGranularity = 'day' | 'week' | 'month' | 'quarter' | 'year';\n\nconst pad = (n: number) => String(n).padStart(2, '0');\n\n/**\n * Format a raw date value (epoch-ms number, numeric string, ISO string, or\n * Date) to a human, sort-stable bucket label per granularity. Returns the input\n * unchanged when it isn't a parseable date, so a non-date value never blanks.\n *\n * year → \"2026\"\n * quarter → \"2026-Q2\"\n * month → \"2026-04\"\n * week → \"2026-04-13\" (ISO date of the bucket)\n * day → \"2026-04-15\"\n *\n * Intentionally UTC-only (ADR-0053 Phase 2): timezone bucketing happens\n * upstream in `bucketDate` / `bucketDateValue`, so by the time a value reaches\n * here it is *already* the reference-zone bucket (often a label string like\n * \"2026-Q2\"). Re-applying a timezone here would shift an already-correct\n * `YYYY-MM-DD` day bucket by a day — this is a pure, idempotent re-labeler.\n */\nexport function formatDateBucket(value: unknown, granularity?: DateGranularity | string): unknown {\n if (value == null || value instanceof Date === false) {\n if (typeof value !== 'number' && typeof value !== 'string') return value;\n }\n // A YEAR bucket's canonical key IS the bare year (\"2026\" / 2026) — which the\n // epoch heuristic below would read as 2026 milliseconds and relabel \"1970\".\n // Being idempotent over already-formatted bucket keys is this function's whole\n // contract, and every other granularity's key already survives the round trip\n // (\"2026-Q2\", \"2026-07\", \"2026-07-15\" all fail the pure-digit test); only the\n // year key collides with it. Recognised before parsing, for both the string\n // and numeric forms drivers return.\n if (granularity === 'year') {\n const y = typeof value === 'number' ? value : Number(String(value).trim());\n if (Number.isInteger(y) && y >= 1000 && y <= 9999) return String(y);\n }\n let d: Date;\n if (value instanceof Date) d = value;\n else if (typeof value === 'number') d = new Date(value);\n else {\n const s = String(value).trim();\n // Pure-digit strings are epoch millis (or seconds); otherwise let Date parse ISO.\n d = /^\\d+$/.test(s) ? new Date(Number(s) < 1e12 ? Number(s) * 1000 : Number(s)) : new Date(s);\n }\n if (Number.isNaN(d.getTime())) return value;\n const y = d.getUTCFullYear();\n const m = d.getUTCMonth(); // 0-11\n switch (granularity) {\n case 'year': return String(y);\n case 'quarter': return `${y}-Q${Math.floor(m / 3) + 1}`;\n case 'month': return `${y}-${pad(m + 1)}`;\n case 'week':\n case 'day':\n default: return `${y}-${pad(m + 1)}-${pad(d.getUTCDate())}`;\n }\n}\n\n/**\n * Replace raw dimension values with display labels, in place.\n *\n * @param baseObject - the dataset's base object (where the dimension fields live)\n * @param dims - selected dimensions as `{ name, field, type?, dateGranularity? }`\n * (row key = `name`)\n * @param rows - result rows, mutated in place\n * @param deps - injected runtime capabilities\n * @param resolveScope - (ADR-0021 D-C, #3602) resolves the referenced object's\n * own read scope for a lookup/master_detail dimension's label fetch. When it\n * throws, that dimension's labels are SKIPPED (fail-closed — the raw id renders\n * instead) rather than fetched unscoped. Omit when no read-scope provider is\n * configured (labels then fetch unscoped, as before — no security in play).\n * @param context - the request's ExecutionContext, forwarded to\n * {@link DimensionLabelDeps.fetchRecordLabels} so the engine's own middleware\n * scopes the per-record label read too — the second belt beside `resolveScope`\n * (#3602)\n */\nexport async function resolveDimensionLabels(\n baseObject: string,\n dims: Array<{ name: string; field: string; type?: string; dateGranularity?: DateGranularity | string }>,\n rows: Record<string, unknown>[],\n deps: DimensionLabelDeps,\n resolveScope?: LabelScopeResolver,\n context?: ExecutionContext,\n): Promise<void> {\n if (!rows.length || !dims.length) return;\n const fields = deps.getObjectFields(baseObject);\n if (!fields) return;\n\n for (const dim of dims) {\n const meta = fields[dim.field];\n\n // ── date: epoch / ISO → human bucket label ────────────────────────\n // A date dimension's grouped value is a raw timestamp (or a bucket start);\n // either way it must render as a readable date, not epoch millis.\n if (dim.type === 'date' || (meta && meta.type === 'date')) {\n for (const row of rows) {\n const formatted = formatDateBucket(row[dim.name], dim.dateGranularity);\n if (formatted != null) row[dim.name] = formatted;\n }\n continue;\n }\n\n if (!meta) continue;\n\n // ── select: value → option label ──────────────────────────────────\n if (Array.isArray(meta.options) && meta.options.length > 0) {\n const labelByValue = new Map<unknown, string>();\n for (const opt of meta.options) {\n if (opt && opt.label != null) labelByValue.set(opt.value, String(opt.label));\n }\n if (labelByValue.size === 0) continue;\n for (const row of rows) {\n const raw = row[dim.name];\n const label = labelByValue.get(raw);\n if (label != null) row[dim.name] = label;\n }\n continue;\n }\n\n // ── lookup / master_detail: id → related record display name ───────\n if (meta.type && LOOKUP_TYPES.has(meta.type) && meta.reference) {\n const ids = Array.from(\n new Set(rows.map((r) => r[dim.name]).filter((v) => v != null)),\n );\n if (ids.length === 0) continue;\n // #3602 — the label lookup reads the REFERENCED object by id. Scope it to\n // that object's own RLS so it never surfaces a related record the target's\n // RLS would hide (leak fires when the referenced object is stricter than\n // the base). Fail closed: if the scope can't be resolved, skip this\n // dimension's labels (raw id renders) rather than fetch unscoped.\n let scope: Record<string, unknown> | null | undefined;\n if (resolveScope) {\n try {\n scope = await resolveScope(meta.reference);\n } catch {\n continue;\n }\n }\n const labelById = await deps.fetchRecordLabels(meta.reference, ids, scope ?? undefined, context);\n if (!labelById || labelById.size === 0) continue;\n for (const row of rows) {\n const label = labelById.get(row[dim.name]);\n if (label != null) row[dim.name] = label;\n }\n }\n }\n}\n\n/**\n * Pick the display field for an object from its field map, by convention:\n * an explicit `name`/`title`/`label` field, else the first text-like field.\n * Returns `undefined` when nothing suitable exists.\n */\nexport function pickDisplayField(\n fields: Record<string, FieldMetaLite> | undefined,\n): string | undefined {\n if (!fields) return undefined;\n for (const preferred of ['name', 'title', 'label']) {\n if (fields[preferred]) return preferred;\n }\n for (const [name, meta] of Object.entries(fields)) {\n if (meta.type === 'text' || meta.type === 'string') return name;\n }\n return undefined;\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n//\n// ADR-0037 Phase 3 — draft data preview: evaluate an AnalyticsQuery over an\n// in-memory row set (the pending `seed` draft's records) instead of the real\n// data engine. This is what lets a Live Canvas dashboard chart REAL numbers\n// from the DRAFTED sample data before anything is published — and because\n// publish materializes the *same* seed, the numbers are continuous across\n// the publish boundary.\n//\n// Scope (deliberately the dataset-query subset, not a general engine):\n// • Mongo-style `where` filters ($eq implicit, $ne/$gt/$gte/$lt/$lte/\n// $in/$nin/$contains, $and/$or/$not)\n// • timeDimensions date-range filtering + granularity bucketing\n// (day/week/month/quarter/year)\n// • group-by dimensions; count / countDistinct / sum / avg / min / max\n// • order + limit/offset\n// Anything beyond (joins via `include`, raw SQL) falls back to the caller's\n// normal execution path — the preview simply doesn't claim it.\n\nimport { calendarPartsInTzOrUtc } from '@objectstack/core';\nimport type { AnalyticsQuery, AnalyticsResult } from '@objectstack/spec/contracts';\nimport type { Cube } from '@objectstack/spec/data';\n\ntype Row = Record<string, unknown>;\n\n// ── Filters (the unified Query DSL subset) ──────────────────────────────────\n\nfunction compare(a: unknown, b: unknown): number {\n if (typeof a === 'number' && typeof b === 'number') return a - b;\n return String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0;\n}\n\nfunction matchOp(value: unknown, op: string, expected: unknown): boolean {\n switch (op) {\n case '$eq': return value === expected || String(value) === String(expected);\n case '$ne': return !(value === expected || String(value) === String(expected));\n case '$gt': return value != null && compare(value, expected) > 0;\n case '$gte': return value != null && compare(value, expected) >= 0;\n case '$lt': return value != null && compare(value, expected) < 0;\n case '$lte': return value != null && compare(value, expected) <= 0;\n case '$in': return Array.isArray(expected) && expected.some((e) => value === e || String(value) === String(e));\n case '$nin': return Array.isArray(expected) && !expected.some((e) => value === e || String(value) === String(e));\n case '$contains': return String(value ?? '').toLowerCase().includes(String(expected ?? '').toLowerCase());\n default: return true; // unknown operator — permissive (preview, reads only)\n }\n}\n\nexport function matchesWhere(row: Row, where: Record<string, unknown> | undefined): boolean {\n if (!where) return true;\n for (const [key, cond] of Object.entries(where)) {\n if (key === '$and') {\n if (!(cond as Row[]).every((c) => matchesWhere(row, c as Row))) return false;\n } else if (key === '$or') {\n if (!(cond as Row[]).some((c) => matchesWhere(row, c as Row))) return false;\n } else if (key === '$not') {\n if (matchesWhere(row, cond as Row)) return false;\n } else if (cond !== null && typeof cond === 'object' && !Array.isArray(cond)) {\n for (const [op, expected] of Object.entries(cond as Row)) {\n if (!matchOp(row[key], op, expected)) return false;\n }\n } else if (!(row[key] === cond || String(row[key]) === String(cond))) {\n return false; // implicit equality\n }\n }\n return true;\n}\n\n// ── Time bucketing ──────────────────────────────────────────────────────────\n\nexport function bucketDate(value: unknown, granularity: string, timezone?: string): string | null {\n const d = new Date(String(value));\n if (Number.isNaN(d.getTime())) return null;\n // ADR-0053 Phase 2: resolve the calendar day in the reference zone so an\n // instant near a tz day-boundary buckets where a user in that zone expects.\n // Unset / 'UTC' / invalid keeps the historical UTC bucketing.\n const { year: y, month, day: dayNum } = calendarPartsInTzOrUtc(d, timezone);\n const m = `${month}`.padStart(2, '0');\n const day = `${dayNum}`.padStart(2, '0');\n switch (granularity) {\n case 'year': return `${y}`;\n case 'quarter': return `${y}-Q${Math.floor((month - 1) / 3) + 1}`;\n case 'month': return `${y}-${m}`;\n case 'week': {\n // Build a UTC date from the zone-shifted parts, then step back to Monday.\n const monday = new Date(Date.UTC(y, month - 1, dayNum));\n const dow = (monday.getUTCDay() + 6) % 7; // Monday=0\n monday.setUTCDate(monday.getUTCDate() - dow);\n return monday.toISOString().slice(0, 10);\n }\n case 'day':\n default:\n return `${y}-${m}-${day}`;\n }\n}\n\n// ── Aggregation ─────────────────────────────────────────────────────────────\n\nfunction aggregate(rows: Row[], metricType: string, field: string): number {\n if (metricType === 'count' || field === '*') {\n if (metricType === 'countDistinct') {\n return new Set(rows.map((r) => r[field]).filter((v) => v != null)).size;\n }\n return rows.length;\n }\n const nums = rows.map((r) => Number(r[field])).filter((n) => Number.isFinite(n));\n switch (metricType) {\n case 'countDistinct': return new Set(rows.map((r) => r[field]).filter((v) => v != null)).size;\n case 'sum': return nums.reduce((a, b) => a + b, 0);\n case 'avg': return nums.length ? nums.reduce((a, b) => a + b, 0) / nums.length : 0;\n case 'min': return nums.length ? Math.min(...nums) : 0;\n case 'max': return nums.length ? Math.max(...nums) : 0;\n default: return nums.length ? nums.reduce((a, b) => a + b, 0) : rows.length;\n }\n}\n\n/**\n * Evaluate `query` over `rows` using the cube's measure/dimension specs.\n * Mirrors the engine strategies' output contract: rows keyed by bare\n * measure/dimension names, `fields` describing each output column.\n */\nexport function evaluateAnalyticsQueryOverRows(\n query: AnalyticsQuery,\n cube: Cube,\n rows: Row[],\n): AnalyticsResult {\n // 1. Row-level filters: `where`, then timeDimension dateRanges.\n let filtered = rows.filter((r) => matchesWhere(r, query.where));\n const timeDims = query.timeDimensions ?? [];\n for (const td of timeDims) {\n const dim = cube.dimensions?.[td.dimension];\n const field = String(dim?.sql ?? td.dimension);\n if (!td.dateRange) continue;\n const [start, end] = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];\n filtered = filtered.filter((r) => {\n const v = String(r[field] ?? '');\n return v >= String(start) && v <= `${end}~`; // '~' > any date char: inclusive end-day\n });\n }\n\n // 2. Grouping keys: each selected dimension (time dims bucketed).\n const dimensions = query.dimensions ?? [];\n const timezone = query.timezone; // ADR-0053 Phase 2: reference tz for bucketing\n const granByDim = new Map(timeDims.filter((t) => t.granularity).map((t) => [t.dimension, t.granularity!]));\n const keyOf = (r: Row): { key: string; values: Row } => {\n const values: Row = {};\n for (const name of dimensions) {\n const dim = cube.dimensions?.[name];\n const field = String(dim?.sql ?? name);\n const raw = r[field];\n const gran = granByDim.get(name) ?? (dim?.type === 'time' && dim.granularities?.length === 1 ? String(dim.granularities[0]) : undefined);\n values[name] = gran ? bucketDate(raw, gran, timezone) : (raw ?? null);\n }\n return { key: JSON.stringify(values), values };\n };\n\n const groups = new Map<string, { values: Row; rows: Row[] }>();\n for (const r of filtered) {\n const { key, values } = keyOf(r);\n const g = groups.get(key) ?? { values, rows: [] };\n g.rows.push(r);\n groups.set(key, g);\n }\n // No dimensions → a single overall group (even over zero rows: count = 0).\n if (dimensions.length === 0 && groups.size === 0) {\n groups.set('{}', { values: {}, rows: [] });\n }\n\n // 3. Aggregate each measure per group.\n const out: Row[] = [];\n for (const g of groups.values()) {\n const row: Row = { ...g.values };\n for (const m of query.measures) {\n const metric = cube.measures?.[m];\n row[m] = aggregate(g.rows, String(metric?.type ?? 'count'), String(metric?.sql ?? '*'));\n }\n out.push(row);\n }\n\n // 4. Order + paging.\n for (const [col, dir] of Object.entries(query.order ?? {}).reverse()) {\n out.sort((a, b) => (dir === 'desc' ? -1 : 1) * compare(a[col], b[col]));\n }\n const offset = query.offset ?? 0;\n const limited = out.slice(offset, query.limit != null ? offset + query.limit : undefined);\n\n return {\n rows: limited,\n fields: [\n ...dimensions.map((d) => ({ name: d, type: 'string' })),\n ...query.measures.map((m) => ({ name: m, type: 'number' })),\n ],\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Cube, FilterCondition } from '@objectstack/spec/data';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\nimport type { IAnalyticsService } from '@objectstack/spec/contracts';\nimport { AnalyticsService } from './analytics-service.js';\nimport type { AnalyticsServiceConfig } from './analytics-service.js';\nimport type { DriverCapabilities } from './strategies/types.js';\nimport { pickDisplayField, type DimensionLabelDeps } from './dimension-labels.js';\n\n/**\n * Minimal IDataEngine surface required for the auto-bridge.\n * ObjectQL exposes:\n * - `aggregate(object, { where, groupBy, aggregations: [{ function, field, alias }] })`\n * - `execute(sql, options)` for raw SQL pass-through (enables NativeSQLStrategy\n * and lets the analytics layer emit JOINs for relation traversal).\n */\ninterface DataEngineLike {\n aggregate(object: string, options: {\n where?: Record<string, unknown>;\n groupBy?: string[];\n aggregations?: Array<{ function: string; field: string; alias: string }>;\n /** Reference timezone (IANA) for date bucketing — ADR-0053 Phase 2. */\n timezone?: string;\n /**\n * `BaseEngineOptions.context` — identity/tenant of the request. The engine\n * merges it into the operation context (`mergeReadContext`), which is what\n * lets its middleware chain inject RLS into `opCtx.ast.where` (#3602).\n */\n context?: ExecutionContext;\n }): Promise<unknown[]>;\n execute?(command: unknown, options?: Record<string, unknown>): Promise<unknown>;\n /** Return the registered object schema (relationship → target + display-label resolution). */\n getObject?(name: string): {\n fields?: Record<string, {\n type?: string;\n reference?: string;\n options?: Array<{ value: unknown; label?: string }>;\n }>;\n /** Federation marker (ADR-0015): set on objects bound to an external datasource. */\n external?: unknown;\n /** The datasource this object is bound to (ADR-0062 D6 external detection). */\n datasource?: string;\n } | undefined;\n /**\n * Resolve the storage driver backing an object (public ObjectQL accessor).\n * Used to delegate temporal filter-value coercion to the driver, which is the\n * single source of truth for how a `Field.date`/`Field.datetime` is stored on\n * the active dialect. The driver may expose `temporalFilterValue(object, field,\n * value)` (SqlDriver does); when absent we leave the value untouched.\n */\n getDriverForObject?(objectName: string): DriverLike | undefined;\n}\n\n/** Minimal driver surface the analytics layer probes for temporal coercion. */\ninterface DriverLike {\n /**\n * Coerce a filter comparand to the column's on-disk storage form\n * (SQLite `Field.datetime` → epoch ms; `Field.date` → YYYY-MM-DD; native\n * timestamp / non-temporal → unchanged). Optional — only SqlDriver implements it.\n */\n temporalFilterValue?(objectName: string, field: string, value: unknown): unknown;\n}\n\n/**\n * Configuration for AnalyticsServicePlugin.\n */\nexport interface AnalyticsServicePluginOptions {\n /** Pre-defined cube definitions (from manifest). */\n cubes?: Cube[];\n /**\n * Probe driver capabilities for a given cube.\n * When omitted, defaults to in-memory only.\n */\n queryCapabilities?: (cubeName: string) => DriverCapabilities;\n /**\n * Execute raw SQL on a driver. Enables NativeSQLStrategy.\n */\n executeRawSql?: (objectName: string, sql: string, params: unknown[]) => Promise<Record<string, unknown>[]>;\n /**\n * Execute ObjectQL aggregate. Enables ObjectQLStrategy.\n */\n executeAggregate?: (objectName: string, options: {\n groupBy?: string[];\n aggregations?: Array<{ field: string; method: string; alias: string }>;\n filter?: Record<string, unknown>;\n /** Reference timezone (IANA) for date bucketing — ADR-0053 Phase 2. */\n timezone?: string;\n /**\n * ADR-0021 D-C (#3602) — the request's ExecutionContext. A custom bridge\n * MUST forward it to its engine so engine-side RLS applies; dropping it is\n * what made the built-in bridge fall open in #3597.\n */\n context?: ExecutionContext;\n }) => Promise<Record<string, unknown>[]>;\n /**\n * ADR-0021 D-C — context-aware per-object read scope (tenant + RLS). The\n * runtime supplies this from its sharing middleware so the analytics raw-SQL\n * path cannot bypass tenant isolation. Receives the request's ExecutionContext\n * and returns the RLS `FilterCondition` for the object (what `RLSCompiler`\n * emits). When omitted, the plugin auto-bridges to a registered `'security'`\n * service exposing `getReadFilter(object, context)` if one is present.\n */\n getReadScope?: (\n objectName: string,\n context?: ExecutionContext,\n ) =>\n | FilterCondition\n | null\n | undefined\n | Promise<FilterCondition | null | undefined>;\n /**\n * ADR-0021 D-C — join allowlist per cube (the dataset's declared `include`).\n * Typically wired from the dataset registry's compiled `allowedRelationships`.\n */\n getAllowedRelationships?: (cubeName: string) => Set<string> | undefined;\n /** Enable debug logging. */\n debug?: boolean;\n}\n\n/**\n * AnalyticsServicePlugin — Kernel plugin for multi-driver analytics.\n *\n * Lifecycle:\n * 1. **init** — Creates `AnalyticsService`, registers as `'analytics'` service.\n * If an existing analytics service is already registered (e.g. MemoryAnalyticsService\n * from dev-plugin), it is captured as the `fallbackService`.\n * 2. **start** — Triggers `'analytics:ready'` hook so other plugins can\n * register cubes or extend the service.\n * 3. **destroy** — Cleans up references.\n *\n * @example\n * ```ts\n * import { LiteKernel } from '@objectstack/core';\n * import { AnalyticsServicePlugin } from '@objectstack/service-analytics';\n *\n * const kernel = new LiteKernel();\n * kernel.use(new AnalyticsServicePlugin({\n * cubes: [ordersCube],\n * queryCapabilities: (cube) => ({ nativeSql: true, objectqlAggregate: true, inMemory: false }),\n * executeRawSql: async (obj, sql, params) => pgPool.query(sql, params).then(r => r.rows),\n * }));\n * await kernel.bootstrap();\n *\n * const analytics = kernel.getService<IAnalyticsService>('analytics');\n * const result = await analytics.query({ cube: 'orders', measures: ['orders.count'] });\n * ```\n */\nexport class AnalyticsServicePlugin implements Plugin {\n name = 'com.objectstack.service-analytics';\n version = '1.0.0';\n type = 'standard' as const;\n dependencies: string[] = [];\n\n private service?: AnalyticsService;\n private readonly options: AnalyticsServicePluginOptions;\n\n constructor(options: AnalyticsServicePluginOptions = {}) {\n this.options = options;\n }\n\n async init(ctx: PluginContext): Promise<void> {\n // Check if there is an existing analytics service (e.g. from dev-plugin)\n let fallbackService: IAnalyticsService | undefined;\n try {\n const existing = ctx.getService<IAnalyticsService>('analytics');\n if (existing && typeof existing.query === 'function') {\n fallbackService = existing;\n ctx.logger.debug('[Analytics] Found existing analytics service, using as fallback');\n }\n } catch {\n // No existing service — that's fine\n }\n\n // Auto-bridge: when caller did not supply executeAggregate, look up the\n // kernel's IDataEngine (registered as 'data' by ObjectQLPlugin) lazily and\n // translate AnalyticsStrategy's `{method, filter}` shape into the engine's\n // `{function, where}` shape. This lets users write\n // `new AnalyticsServicePlugin({ cubes })`\n // without re-implementing the bridge in every app.\n let executeAggregate = this.options.executeAggregate;\n let autoBridged = false;\n if (!executeAggregate) {\n const tryGetDataEngine = (): DataEngineLike | undefined => {\n try {\n const svc = ctx.getService<DataEngineLike>('data');\n return svc && typeof svc.aggregate === 'function' ? svc : undefined;\n } catch {\n return undefined;\n }\n };\n // Probe now (warn if missing) but resolve at call time so plugin order\n // does not matter as long as 'data' exists by the time a query runs.\n if (!tryGetDataEngine()) {\n ctx.logger.warn(\n '[Analytics] No \"data\" service registered yet at init; ' +\n 'will retry per-query. Register ObjectQLPlugin or pass executeAggregate.',\n );\n }\n executeAggregate = async (objectName, { groupBy, aggregations, filter, timezone, context }) => {\n const engine = tryGetDataEngine();\n if (!engine) {\n throw new Error(\n '[Analytics] Cannot execute aggregate: no IDataEngine (\"data\") service is registered. ' +\n 'Add ObjectQLPlugin to the kernel or supply AnalyticsServicePlugin({ executeAggregate }).',\n );\n }\n const rows = await engine.aggregate(objectName, {\n where: filter,\n groupBy,\n aggregations: aggregations?.map((a) => ({\n function: a.method,\n field: a.field,\n alias: a.alias,\n })),\n // ADR-0053 Phase 2: thread the reference tz so date buckets resolve on\n // that zone's calendar days (engine buckets in-memory when non-UTC).\n timezone,\n // ADR-0021 D-C (#3602): thread the caller's identity so the engine's\n // middleware chain scopes the read itself. `BaseEngineOptions.context`\n // is `.optional()`, so nothing ever forced this bridge to pass it —\n // and it did not, which is how an authenticated aggregate reached the\n // engine with no principal and plugin-security fell open (#3597).\n context,\n });\n return rows as Record<string, unknown>[];\n };\n autoBridged = true;\n }\n\n // Auto-bridge raw SQL when the data engine exposes `execute()` and the\n // caller did not supply their own `executeRawSql`. This unlocks\n // NativeSQLStrategy (priority 10) which can emit `LEFT JOIN`s for\n // dotted dimension/measure references like `account.industry`.\n let executeRawSql = this.options.executeRawSql;\n let autoBridgedRawSql = false;\n if (!executeRawSql) {\n const tryGetExecutor = (): DataEngineLike | undefined => {\n try {\n const svc = ctx.getService<DataEngineLike>('data');\n return svc && typeof svc.execute === 'function' ? svc : undefined;\n } catch {\n return undefined;\n }\n };\n // Always wire the bridge — resolution happens at call time, mirroring\n // the executeAggregate auto-bridge above. This way plugin-init order\n // does not matter as long as `data` exists by the time a query runs.\n executeRawSql = async (_objectName, sql, params) => {\n const engine = tryGetExecutor();\n if (!engine || !engine.execute) {\n throw new Error(\n '[Analytics] Cannot execute raw SQL: no IDataEngine (\"data\") service with execute() is registered.',\n );\n }\n // NativeSQLStrategy emits `$1, $2, …` placeholders. Knex (used by\n // driver-sql) speaks `?` placeholders, so translate.\n const knexSql = sql.replace(/\\$(\\d+)/g, '?');\n const result = await engine.execute(knexSql, { args: params });\n // A driver that cannot run SQL (e.g. the in-memory driver) returns\n // null from execute(). Silently mapping that to [] made EVERY dataset\n // query on such environments report \"No rows\" while looking healthy\n // (HTTP 200, compiled SQL attached). Throw a TYPED error instead so\n // the orchestrator can fall back to an aggregate-based strategy —\n // never fabricate an empty result.\n if (result === null || result === undefined) {\n const err = new Error(\n '[Analytics] The \"data\" engine\\'s driver returned null for raw SQL — ' +\n 'this driver does not support SQL execution. The query will fall back ' +\n 'to an aggregate-based strategy when one is available.',\n ) as Error & { code: string };\n err.code = 'RAW_SQL_UNSUPPORTED';\n throw err;\n }\n if (Array.isArray(result)) return result as Record<string, unknown>[];\n if (typeof result === 'object' && 'rows' in (result as Record<string, unknown>)) {\n return (result as { rows: Record<string, unknown>[] }).rows;\n }\n return [];\n };\n autoBridgedRawSql = true;\n }\n\n // Default capabilities: when we have an aggregate bridge, advertise\n // ObjectQL support so ObjectQLStrategy is selected. Callers can still\n // override via options.queryCapabilities.\n const queryCapabilities = this.options.queryCapabilities\n ?? (() => ({\n nativeSql: !!executeRawSql,\n objectqlAggregate: !!executeAggregate,\n inMemory: false,\n }));\n\n // ADR-0021 D-C — wire the read-scope provider. Prefer an explicit option;\n // otherwise auto-bridge to a registered `'security'` service that exposes\n // `getReadFilter(object, context)` (resolved at call time so plugin-init\n // order does not matter). This keeps analytics decoupled from security.\n interface SecurityReadFilter {\n getReadFilter(\n object: string,\n context?: ExecutionContext,\n ):\n | FilterCondition\n | null\n | undefined\n | Promise<FilterCondition | null | undefined>;\n }\n let getReadScope = this.options.getReadScope;\n let autoBridgedReadScope = false;\n let securityPresentAtInit = false;\n if (!getReadScope) {\n const trySecurity = (): SecurityReadFilter | undefined => {\n try {\n const svc = ctx.getService<SecurityReadFilter>('security');\n return svc && typeof svc.getReadFilter === 'function' ? svc : undefined;\n } catch {\n return undefined;\n }\n };\n // ALWAYS wire the bridge — resolution happens at call time, mirroring the\n // executeAggregate / executeRawSql auto-bridges above. Gating the\n // ASSIGNMENT on an init-time probe (as this did) made analytics RLS\n // silently plugin-ORDER-DEPENDENT: a kernel that registers this plugin\n // before the security plugin got NO read-scope provider at all, so every\n // strategy ran unscoped and only a WARN marked it. The repo's own\n // `bootStack` harness registers in exactly that order, which is why no\n // dogfood test could ever observe analytics RLS.\n securityPresentAtInit = !!trySecurity();\n getReadScope = (object, context) => trySecurity()?.getReadFilter(object, context);\n autoBridgedReadScope = true;\n }\n\n // ADR-0021 — relationship → target-object resolver. A dataset's `include`\n // names lookup/master_detail FIELDS on the base object; the joined TABLE is\n // each field's `reference` target (which can differ from the field name,\n // e.g. lookup `account` → object `crm_account`). Resolve from the 'data'\n // engine's object schema at compile time so cross-object joins target the\n // right table. Resolved lazily so plugin-init order doesn't matter.\n const relationshipResolver = (baseObject: string, relationshipName: string): string | undefined => {\n const engine = (() => {\n try {\n const svc = ctx.getService<DataEngineLike>('data');\n return svc && typeof svc.getObject === 'function' ? svc : undefined;\n } catch { return undefined; }\n })();\n const obj = engine?.getObject?.(baseObject);\n const field = obj?.fields?.[relationshipName];\n if (field && (field.type === 'lookup' || field.type === 'master_detail') && field.reference) {\n return field.reference;\n }\n // Unknown to the schema — fall back to the relationship name as the table\n // (legacy same-name convention). Returning undefined would make the\n // compiler reject the dataset; the name-as-table fallback is safer for\n // engines that don't expose getObject.\n return engine ? undefined : relationshipName;\n };\n\n // ADR-0021 — dimension display-label resolution. `queryDataset` groups by a\n // dimension's raw stored value; for `select` fields the user-facing text is\n // the option label, and for `lookup`/`master_detail` fields it's the related\n // record's display name. Wire the two low-level capabilities the resolver\n // needs from the 'data' engine (resolved lazily so plugin-init order is free):\n // - field metadata (select options + lookup target), via getObject\n // - id→name pairs, via the executeAggregate bridge (group by id + name)\n const dataEngine = (): DataEngineLike | undefined => {\n try {\n const svc = ctx.getService<DataEngineLike>('data');\n return svc && typeof svc.getObject === 'function' ? svc : undefined;\n } catch { return undefined; }\n };\n const labelResolver: DimensionLabelDeps = {\n getObjectFields: (objectName) => dataEngine()?.getObject?.(objectName)?.fields,\n fetchRecordLabels: async (targetObject, ids, scope, context) => {\n const map = new Map<unknown, string>();\n const displayField = pickDisplayField(dataEngine()?.getObject?.(targetObject)?.fields);\n if (!displayField || !executeAggregate || ids.length === 0) return map;\n // #3680 — the sort-key pass hands over the PRE-window id set (every\n // grouped value, not just the displayed page), so a high-cardinality\n // lookup dimension can push thousands of ids through here. Chunk the\n // `$in` so the bound-parameter count stays under every driver's limit\n // (SQLite's historic floor is 999 variables).\n const CHUNK = 500;\n for (let i = 0; i < ids.length; i += CHUNK) {\n // #3602 — AND the referenced object's own read scope into the id filter,\n // with `$and` (never key-merge) so it cannot be displaced by the id\n // predicate — the same composition the strategy uses for the aggregate.\n // Without it this per-record read leaks display names the target's RLS\n // would hide (fires when the referenced object is stricter than the base).\n const idFilter: Record<string, unknown> = { id: { $in: ids.slice(i, i + CHUNK) } };\n const filter = scope ? { $and: [idFilter, scope] } : idFilter;\n // Group by (id, displayField) — one row per record — reusing the aggregate\n // bridge rather than adding a record-fetch capability. A count keeps engines\n // that require ≥1 aggregation happy; the count itself is unused.\n const rows = await executeAggregate(targetObject, {\n groupBy: ['id', displayField],\n aggregations: [{ field: 'id', method: 'count', alias: '_c' }],\n filter,\n // #3602 second belt — `scope` above is the analytics layer's own\n // predicate on this per-record read; the context makes the engine's\n // middleware scope it as well.\n context,\n });\n for (const r of rows) {\n if (r.id != null && r[displayField] != null) map.set(r.id, String(r[displayField]));\n }\n }\n return map;\n },\n };\n\n // ADR-0037 P3 — draft data preview: resolve the PENDING seed draft's rows\n // for an object via the kernel protocol (state:'draft' read — a published\n // seed's rows are already in the real table and must NOT overlay). Lazy\n // service lookup so plugin order doesn't matter; null ⇒ no pending seed ⇒\n // queryDataset falls through to live data.\n const draftRowsResolver = async (objectName: string): Promise<Record<string, unknown>[] | null> => {\n type ProtocolLike = {\n getMetaItems?(req: { type: string; previewDrafts?: boolean }): Promise<unknown>;\n getMetaItem?(req: { type: string; name: string; state?: string }): Promise<unknown>;\n };\n let protocol: ProtocolLike | undefined;\n try {\n protocol = ctx.getService<ProtocolLike>('protocol');\n } catch { return null; }\n if (!protocol?.getMetaItems || !protocol.getMetaItem) return null;\n const res = await protocol.getMetaItems({ type: 'seed', previewDrafts: true }).catch(() => null);\n const list = Array.isArray(res)\n ? res\n : (res && typeof res === 'object' && Array.isArray((res as { items?: unknown[] }).items)\n ? (res as { items: unknown[] }).items\n : []);\n const rows: Record<string, unknown>[] = [];\n let pending = false;\n for (const entry of list) {\n const body = ((entry as { item?: unknown })?.item ?? entry) as { name?: string; object?: string } | null;\n if (!body?.name || body.object !== objectName) continue;\n // Only a PENDING draft row qualifies; getMetaItem({state:'draft'})\n // throws no_draft when the seed is already published.\n const draft = await protocol.getMetaItem({ type: 'seed', name: body.name, state: 'draft' }).catch(() => null);\n const draftBody = (draft as { item?: { records?: unknown[] } } | null)?.item;\n if (!draftBody) continue;\n pending = true;\n for (const r of Array.isArray(draftBody.records) ? draftBody.records : []) {\n if (r && typeof r === 'object') rows.push(r as Record<string, unknown>);\n }\n }\n return pending ? rows : null;\n };\n\n // Temporal storage-form coercion (fixes the SQLite datetime \"No rows\" bug).\n // The raw-SQL strategy binds dashboard relative-date tokens (already expanded\n // to ISO strings) directly, bypassing the driver's CRUD coercion. Delegate to\n // the driver — the single source of truth for the on-disk storage convention —\n // so a `Field.datetime` ISO comparand becomes epoch ms on SQLite, while\n // `Field.date` text and native-timestamp (Postgres) columns pass through\n // unchanged. Resolved at call time so plugin-init order does not matter.\n const coerceTemporalFilterValue = (\n objectName: string,\n fieldName: string,\n value: unknown,\n ): unknown => {\n try {\n const svc = ctx.getService<DataEngineLike>('data');\n const driver = svc?.getDriverForObject?.(objectName);\n if (driver && typeof driver.temporalFilterValue === 'function') {\n return driver.temporalFilterValue(objectName, fieldName, value);\n }\n } catch {\n // No data engine / driver, or it doesn't support coercion — leave the\n // value as-is (today's behaviour; safe for text/native-timestamp paths).\n }\n return value;\n };\n\n const config: AnalyticsServiceConfig = {\n cubes: this.options.cubes,\n logger: ctx.logger,\n queryCapabilities,\n executeRawSql,\n executeAggregate,\n fallbackService,\n getReadScope,\n getAllowedRelationships: this.options.getAllowedRelationships,\n coerceTemporalFilterValue,\n relationshipResolver,\n labelResolver,\n // ADR-0053 — source-field currency metadata for the measure currency chain.\n measureCurrency: (object: string, field: string) => {\n const f = dataEngine()?.getObject?.(object)?.fields?.[field] as\n | { type?: string; currencyConfig?: { defaultCurrency?: string } }\n | undefined;\n return f ? { type: f.type, defaultCurrency: f.currencyConfig?.defaultCurrency } : undefined;\n },\n // ADR-0062 D6 — a federated object carries an `external` block (ADR-0015).\n // Reported so NativeSQLStrategy declines it (its hand-compiled FROM would\n // hit the wrong physical table) and the driver-correct ObjectQL path runs.\n isExternalObject: (objectName: string) => {\n const obj = dataEngine()?.getObject?.(objectName);\n return !!(obj && obj.external != null);\n },\n // [#3867] Existence probe for the cube auto-inference gate. Reads the\n // same schema registry the data path's #3770 gate consults, through the\n // engine accessor this bridge already uses above — so \"which objects\n // exist\" has one answer across /data and /analytics.\n //\n // `dataEngine()` resolves lazily and may be absent entirely (analytics\n // installed without a data engine). Reporting `false` there would 404\n // every cube, so an unresolvable engine reports `true` — \"cannot answer,\n // do not block\" — mirroring the tiering #3770 took on the data path.\n isRegisteredObject: (name: string) => {\n const engine = dataEngine();\n if (!engine) return true;\n return engine.getObject?.(name) != null;\n },\n draftRowsResolver,\n };\n\n if (autoBridgedReadScope && securityPresentAtInit) {\n ctx.logger.info('[Analytics] Auto-bridged getReadScope → \"security\" service (getReadFilter)');\n } else if (autoBridgedReadScope) {\n // The bridge IS wired and will resolve at call time — this is only a\n // heads-up that security had not registered yet at our init. It becomes a\n // real problem only if no security service ever appears.\n ctx.logger.info(\n '[Analytics] getReadScope bridged to the \"security\" service; that service is not ' +\n 'registered yet at init and will be resolved per query (plugin order is not significant).',\n );\n } else if (!getReadScope) {\n ctx.logger.warn(\n '[Analytics] No getReadScope configured and no \"security\" service with getReadFilter found — ' +\n 'analytics queries will NOT enforce tenant/RLS scoping (ADR-0021 D-C). ' +\n 'Supply getReadScope or register a security service in multi-tenant deployments.',\n );\n }\n\n if (autoBridged) {\n ctx.logger.info('[Analytics] Auto-bridged executeAggregate → \"data\" service (IDataEngine)');\n }\n if (autoBridgedRawSql) {\n ctx.logger.info('[Analytics] Auto-bridged executeRawSql → \"data\" service (IDataEngine.execute)');\n }\n\n this.service = new AnalyticsService(config);\n\n // Register or replace the analytics service\n if (fallbackService) {\n ctx.replaceService('analytics', this.service);\n } else {\n ctx.registerService('analytics', this.service);\n }\n\n if (this.options.debug) {\n ctx.hook('analytics:beforeQuery', async (query: unknown) => {\n ctx.logger.debug('[Analytics] Before query', { query });\n });\n }\n\n ctx.logger.info('[Analytics] Service initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n if (!this.service) return;\n\n // Notify other plugins that analytics is ready\n await ctx.trigger('analytics:ready', this.service);\n\n ctx.logger.info(\n `[Analytics] Service started with ${this.service.cubeRegistry.size} cubes: ` +\n `${this.service.cubeRegistry.names().join(', ') || '(none)'}`,\n );\n }\n\n async destroy(): Promise<void> {\n this.service = undefined;\n }\n}\n"],"mappings":"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","names":["n","y"]}
|
|
1
|
+
{"version":3,"sources":["../src/analytics-service.ts","../src/cube-registry.ts","../src/strategies/filter-normalizer.ts","../src/read-scope-sql.ts","../src/strategies/native-sql-strategy.ts","../src/strategies/objectql-strategy.ts","../src/strategies/cross-object-rebucket.ts","../src/dataset-compiler.ts","../src/dataset-executor.ts","../src/dimension-labels.ts","../src/preview-evaluator.ts","../src/plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n IAnalyticsService,\n AnalyticsQuery,\n AnalyticsResult,\n CubeMeta,\n DatasetSelection,\n} from '@objectstack/spec/contracts';\nimport { percentScaleOf, type Cube, type FilterCondition } from '@objectstack/spec/data';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\nimport type { Dataset } from '@objectstack/spec/ui';\nimport type { Logger } from '@objectstack/spec/contracts';\nimport { createLogger, bucketKeyToCalendarRange, zonedDateStartToUtcMs } from '@objectstack/core';\nimport { CubeRegistry } from './cube-registry.js';\nimport type { AnalyticsStrategy, AnalyticsDriverCapabilities, StrategyContext } from './strategies/types.js';\nimport { NativeSQLStrategy } from './strategies/native-sql-strategy.js';\nimport { ObjectQLStrategy } from './strategies/objectql-strategy.js';\nimport { compileDataset, type CompiledDataset, type RelationshipResolver } from './dataset-compiler.js';\nimport { DatasetExecutor, resolveDimensionGranularity, type DateGranularityValue } from './dataset-executor.js';\nimport {\n resolveDimensionLabels,\n createOrderLabelResolver,\n withLabelFetchCache,\n type DimensionLabelDeps,\n} from './dimension-labels.js';\nimport { evaluateAnalyticsQueryOverRows } from './preview-evaluator.js';\n\n/**\n * Analytics result augmented with drill-through metadata (ADR-0021 D2; see\n * queryDataset). Carried alongside `rows` so the host can drill a clicked bucket\n * back to the underlying records without the renderer knowing field mappings.\n */\ntype AnalyticsResultWithDrill = AnalyticsResult & {\n /** The dataset's base object — the host drills into its records. */\n object?: string;\n /** Selected drillable dimension NAME → underlying object FIELD name. */\n dimensionFields?: Record<string, string>;\n /**\n * RAW grouped values per row, aligned to `rows` by index — each a map of\n * drillable dimension NAME → stored value (BEFORE label resolution rewrote\n * `rows[i][dim]` to the display label). The exact-match drill filter is built\n * from these, never from the display labels.\n */\n drillRawRows?: Array<Record<string, unknown>>;\n /**\n * RAW grouped values for the totals/subtotal rows (#3214), the totals-side\n * companion to `drillRawRows`: `drillRawTotals[i]` aligns to `result.totals[i]`\n * and `drillRawTotals[i][j]` to `result.totals[i].rows[j]`. Each map holds that\n * grouping's DRILLABLE dimension NAME → stored value, snapshotted in the SAME\n * pre-label-resolution pass (the totals loop below overwrites a subtotal row's\n * dimension value with its display label just like the data rows). Restricted\n * to the drillable dims present in the grouping, so the grand-total grouping\n * (`[]`) contributes an empty map per row — which keeps the index alignment\n * intact and correctly drills the whole (unfiltered) object.\n */\n drillRawTotals?: Array<Array<Record<string, unknown>>>;\n /**\n * #1752 — half-open date-range drill scope per row, the RANGE companion to\n * `drillRawRows` (which handles equality dims). A time-bucketed date\n * dimension (`dateGranularity`) groups a SPAN of records into one bucket\n * (\"2026-Q2\"), so its drill needs `[gte, lt)`, not equality — the humanized\n * bucket can't be exact-matched (which is why date dims are excluded from\n * `dimensionFields`/`drillRawRows`). Aligned to `rows` by index; each entry\n * maps a drillable date-dimension NAME → `{ field, gte, lt }` with `gte`\n * inclusive and `lt` exclusive (bounds as `YYYY-MM-DD`). Present only for\n * buckets whose boundaries are unambiguous — a `datetime` field under a\n * non-UTC reference timezone is omitted (host drills an unscoped superset)\n * until instant-boundary support lands.\n */\n drillRanges?: Array<Record<string, { field: string; gte: string; lt: string }>>;\n};\n\n/**\n * Detect the \"backing object/table isn't present in this kernel\" class of\n * error so a dataset query can degrade to an empty result instead of failing\n * the widget with a 500. Matches the missing-relation signatures across the\n * drivers ObjectStack runs on (sqlite/libsql, postgres, mysql) plus the\n * framework's own unknown-object signal. Deliberately scoped to MISSING SOURCE\n * (table/object/relation) — not column/syntax errors, which stay hard failures\n * so real query bugs still surface.\n */\nfunction isMissingSourceError(err: unknown): boolean {\n const msg = String((err as { message?: unknown })?.message ?? err ?? '').toLowerCase();\n return (\n msg.includes('no such table') || // sqlite / libsql\n (msg.includes('relation') && msg.includes('does not exist')) || // postgres\n msg.includes(\"doesn't exist\") || // mysql (\"table ... doesn't exist\")\n msg.includes('not registered') || // framework: object not in registry\n msg.includes('unknown object') ||\n msg.includes('is not a registered object')\n );\n}\n\n/**\n * [#4437] A name that is a plain column/table identifier and nothing else.\n * Anything with a dot, a paren, whitespace or an operator is a SQL EXPRESSION\n * (or a cross-object reference) whose parts this layer cannot attribute to a\n * single field — such measures pass the source-field gate untouched.\n */\nconst BARE_IDENTIFIER = /^[a-z_][a-z0-9_]*$/i;\n\n/**\n * Configuration for AnalyticsService.\n */\nexport interface AnalyticsServiceConfig {\n /** Pre-defined cube definitions (from manifest). */\n cubes?: Cube[];\n /** Logger instance. */\n logger?: Logger;\n /**\n * Probe driver capabilities for the object that backs a cube.\n * The service calls this function to decide which strategy can handle a query.\n */\n queryCapabilities?: (cubeName: string) => AnalyticsDriverCapabilities;\n /**\n * Execute raw SQL on the driver for a given object.\n * Required for NativeSQLStrategy.\n */\n executeRawSql?: (objectName: string, sql: string, params: unknown[]) => Promise<Record<string, unknown>[]>;\n /**\n * Execute an ObjectQL aggregate query.\n * Required for ObjectQLStrategy.\n */\n executeAggregate?: (objectName: string, options: {\n groupBy?: string[];\n aggregations?: Array<{ field: string; method: string; alias: string }>;\n filter?: Record<string, unknown>;\n /** Reference timezone (IANA) for date bucketing — ADR-0053 Phase 2. */\n timezone?: string;\n /**\n * ADR-0021 D-C (#3602) — the request's ExecutionContext. Bridges MUST\n * forward it to `engine.aggregate` so engine-side RLS applies; see\n * `StrategyContext.executeAggregate` for why this is a second belt rather\n * than a replacement for `getReadScope`.\n */\n context?: ExecutionContext;\n }) => Promise<Record<string, unknown>[]>;\n /**\n * Fallback IAnalyticsService (e.g. MemoryAnalyticsService).\n * Used by InMemoryStrategy.\n */\n fallbackService?: IAnalyticsService;\n /**\n * Custom strategies to add/replace the defaults.\n * They are merged with the built-in strategies and sorted by priority.\n */\n strategies?: AnalyticsStrategy[];\n /**\n * ADR-0021 D-C — context-aware per-object read scope (tenant + RLS). Supplied\n * by the runtime that owns the sharing middleware; receives the current\n * request's ExecutionContext and returns the RLS `FilterCondition` for the\n * object (exactly what `RLSCompiler` emits). The service binds the active\n * context per query and the strategy compiles the filter into alias-qualified\n * SQL injected into every base and joined table.\n *\n * MAY be async: the production bridge resolves RLS from the `security`\n * service's `getReadFilter`, which can hit the database. The service\n * pre-resolves the scope for every base + joined object of a query (before\n * the synchronous SQL builder runs), so a sync return still works unchanged.\n */\n getReadScope?: (\n objectName: string,\n context?: ExecutionContext,\n ) =>\n | FilterCondition\n | null\n | undefined\n | Promise<FilterCondition | null | undefined>;\n /**\n * ADR-0021 D-C — join allowlist per cube (the dataset's declared `include`).\n * Joins outside this set are rejected by the strategy. Compiled datasets\n * (via `queryDataset`/`registerDataset`) supply this automatically; this\n * config hook is a fallback for legacy hand-authored cubes.\n */\n getAllowedRelationships?: (cubeName: string) => Set<string> | undefined;\n /**\n * Coerce a filter comparand to a temporal column's storage form so a\n * relative-date / ISO-string value compares correctly on the active driver\n * (SQLite `Field.datetime` → epoch ms; `Field.date` / native timestamp →\n * unchanged). Threaded into the StrategyContext and consulted by\n * `NativeSQLStrategy` when binding filter values. See the contract docs on\n * `StrategyContext.coerceTemporalFilterValue` for the full rationale.\n */\n coerceTemporalFilterValue?: (objectName: string, fieldName: string, value: unknown) => unknown;\n /**\n * Normalise the COLUMN side of the same comparison to that storage form — the\n * other half of the fix, needed because a SQLite `Field.datetime` holds both an\n * INTEGER epoch (a `Date` write) and ISO TEXT (a REST/JSON write, a `NOW()`\n * default) at once, so coercing only the comparand matches one of them and\n * misses the other (#3912). See `StrategyContext.coerceTemporalFilterColumn`.\n */\n coerceTemporalFilterColumn?: (objectName: string, fieldName: string, columnSql: string) => string;\n /**\n * ADR-0062 D6 — report whether an object is federated (external datasource).\n * Threaded into the StrategyContext so `NativeSQLStrategy` declines external\n * objects (which it would otherwise query against the wrong physical table),\n * routing them to the driver-correct ObjectQL aggregate path instead. See\n * `StrategyContext.isExternalObject`.\n */\n isExternalObject?: (objectName: string) => boolean;\n /**\n * [#3867] Is `name` a registered object in this kernel's schema registry?\n *\n * Consulted by {@link AnalyticsService.ensureCube} on the auto-inference\n * path only. When no Cube is registered under the queried name, the service\n * infers a minimal one whose `sql` IS that name — the intended \"metric over\n * an object\" path (an `object-metric` KPI widget queries `crm_account`\n * without anyone authoring a Cube). Without this hook that inference accepts\n * ANY string, so an arbitrary physical table name reached the driver: the\n * analytics-side twin of the data-path gap closed in #3770.\n *\n * Optional, and absence means \"skip the check\" — same tiering as #3770's\n * `assertObjectRegistered`: with no registry to consult the question cannot\n * be answered, and failing closed would break every embedding that runs\n * analytics without a data engine. The production bridge in `plugin.ts`\n * always wires it.\n */\n isRegisteredObject?: (name: string) => boolean;\n /**\n * [#4437] The FIELD NAMES `objectName` declares, or `undefined` when nothing\n * authoritative can answer.\n *\n * Consulted by {@link AnalyticsService.ensureCube} to validate the SOURCE\n * FIELD a measure resolves to BEFORE any SQL is built. `inferMeasure` maps a\n * suffix convention onto a field name (`ghost_sum` → `SUM(ghost)`) and used\n * to accept any spelling, so a typo'd measure reached the driver as a column\n * and came back as an opaque `500 SQLITE_ERROR` — a driver error class on the\n * wire for a caller-shaped mistake (ADR-0112). The DATA route already refuses\n * the same mistake with a `400 INVALID_FIELD` naming the field (#4315/#4254);\n * this hook is what lets the ANALYTICS route give the same answer.\n *\n * Same tiering as {@link isRegisteredObject}: absence means \"skip the check\"\n * (registry-less hosts, engine doubles, external datasources whose columns\n * are not mirrored locally). The production bridge in `plugin.ts` wires it\n * from the same schema registry the data path's gate reads, so \"which fields\n * exist\" has ONE answer across `/data` and `/analytics`.\n */\n getObjectFieldNames?: (objectName: string) => readonly string[] | undefined;\n /**\n * ADR-0021 — optional object-graph resolver used when compiling datasets:\n * `(baseObject, relationshipName) => relatedObjectName | undefined`. When\n * provided, `queryDataset` validates that every declared `include` exists.\n */\n relationshipResolver?: RelationshipResolver;\n /**\n * Resolve the metadata of a dimension's or measure's SOURCE FIELD on the\n * dataset's base object — the one seam through which display semantics that\n * live on the field reach the result columns. `undefined` for an unknown\n * field. Feeds three chains:\n *\n * - ADR-0053 currency: a monetary measure that omits an explicit `currency`\n * falls back to the field's declared currency, then the tenant default\n * (`ctx.currency`). Non-`currency` fields never get a code.\n * - Percent scale (objectui#3136): a measure over a `percent` field inherits\n * that field's storage scale via `percentScaleOf`, so a renderer scales by\n * declared metadata instead of guessing from the value.\n * - Date bucketing: a date vs datetime dimension drills by the right bound.\n */\n sourceFieldMeta?: (object: string, field: string) => { type?: string; defaultCurrency?: string; max?: number } | undefined;\n /** Pre-defined datasets to compile + register at construction (ADR-0021). */\n datasets?: Dataset[];\n /**\n * ADR-0021 — resolve raw dimension values to human display labels. When\n * provided, `queryDataset` post-processes result rows so a `select` dimension\n * shows its option label (not the stored value) and a `lookup`/`master_detail`\n * dimension shows the related record's display name (not the FK id). Injected\n * by the plugin from the `data` engine; omit to keep raw values.\n */\n labelResolver?: DimensionLabelDeps;\n\n /**\n * ADR-0037 Phase 3 — draft data preview. Resolve the PENDING `seed` draft\n * rows for an object (returns null when the object has no pending seed).\n * When provided and `queryDataset` is called with `previewDrafts`, the\n * selection is evaluated over these rows in memory instead of the engine —\n * the Live Canvas charts real numbers from the drafted sample data, and\n * because publish materializes the SAME seed, the numbers are continuous\n * across the publish boundary. Reads only; never touches physical tables.\n */\n draftRowsResolver?: (\n objectName: string,\n context?: ExecutionContext,\n ) => Promise<Record<string, unknown>[] | null>;\n}\n\n/**\n * Default capabilities when probing is not configured — assumes in-memory only.\n */\nconst DEFAULT_CAPABILITIES: AnalyticsDriverCapabilities = {\n nativeSql: false,\n objectqlAggregate: false,\n inMemory: true,\n};\n\n/**\n * AnalyticsService — Multi-driver analytics orchestrator.\n *\n * Implements `IAnalyticsService` by delegating to a priority-ordered\n * strategy chain:\n *\n * | Priority | Strategy | Condition |\n * |:---:|:---|:---|\n * | P1 (10) | NativeSQLStrategy | Driver supports raw SQL |\n * | P2 (20) | ObjectQLStrategy | Driver supports aggregate AST |\n * | P3 (30) | (custom / InMemoryStrategy from driver-memory) | Injected by user |\n *\n * When `fallbackService` is configured, an internal delegate strategy\n * is automatically appended at priority 30 as a safety net.\n *\n * The service also owns a `CubeRegistry` for metadata discovery and\n * auto-inference from object schemas.\n */\nexport class AnalyticsService implements IAnalyticsService {\n private readonly strategies: AnalyticsStrategy[];\n /** Context-independent part of the StrategyContext (no per-request scope). */\n private readonly baseCtx: StrategyContext;\n /** Context-aware read-scope provider (bound to the request's context per call). */\n private readonly readScopeProvider?: AnalyticsServiceConfig['getReadScope'];\n /** Compiled datasets by name — feeds the join allowlist (D-C) and queryDataset. */\n private readonly datasetRegistry = new Map<string, CompiledDataset>();\n /** Optional object-graph resolver used when compiling datasets. */\n private readonly relationshipResolver?: RelationshipResolver;\n private readonly sourceFieldMeta?: AnalyticsServiceConfig['sourceFieldMeta'];\n /** Optional dimension display-label resolver (select options / lookup names). */\n private readonly labelResolver?: DimensionLabelDeps;\n /** ADR-0037 P3: pending-seed row resolver for draft data preview. */\n private readonly draftRowsResolver?: AnalyticsServiceConfig['draftRowsResolver'];\n /** [#3867] Schema-registry probe gating cube auto-inference. */\n private readonly isRegisteredObject?: AnalyticsServiceConfig['isRegisteredObject'];\n /** [#4437] Field-name probe gating measure source-field resolution. */\n private readonly getObjectFieldNames?: AnalyticsServiceConfig['getObjectFieldNames'];\n /** [#3867] One-shot flag for the {@link assertInferableCube} stand-down warning. */\n private warnedNoObjectRegistry = false;\n readonly cubeRegistry: CubeRegistry;\n private readonly logger: Logger;\n\n constructor(config: AnalyticsServiceConfig = {}) {\n this.logger = config.logger || createLogger({ level: 'info', format: 'pretty' });\n this.cubeRegistry = new CubeRegistry();\n\n // Register pre-defined cubes\n if (config.cubes) {\n this.cubeRegistry.registerAll(config.cubes);\n }\n\n this.readScopeProvider = config.getReadScope;\n this.relationshipResolver = config.relationshipResolver;\n this.sourceFieldMeta = config.sourceFieldMeta;\n this.labelResolver = config.labelResolver;\n this.draftRowsResolver = config.draftRowsResolver;\n this.isRegisteredObject = config.isRegisteredObject;\n this.getObjectFieldNames = config.getObjectFieldNames;\n\n // Compile + register pre-defined datasets (ADR-0021).\n if (config.datasets) {\n for (const ds of config.datasets) {\n try {\n this.registerDataset(ds);\n } catch (e) {\n this.logger?.warn?.(`[Analytics] Failed to register dataset \"${ds?.name}\": ${String((e as Error)?.message ?? e)}`);\n }\n }\n }\n\n // Build the context-independent strategy context. `getReadScope` is bound\n // per query in `callCtx(context)` so it can resolve the active tenant.\n this.baseCtx = {\n getCube: (name) => this.cubeRegistry.get(name),\n queryCapabilities: config.queryCapabilities || (() => DEFAULT_CAPABILITIES),\n executeRawSql: config.executeRawSql,\n executeAggregate: config.executeAggregate,\n fallbackService: config.fallbackService,\n // Prefer a compiled dataset's declared relationships (D-C join allowlist);\n // fall back to any explicitly-configured provider for legacy cubes.\n getAllowedRelationships: (cubeName: string) =>\n this.datasetRegistry.get(cubeName)?.allowedRelationships\n ?? config.getAllowedRelationships?.(cubeName),\n coerceTemporalFilterValue: config.coerceTemporalFilterValue,\n coerceTemporalFilterColumn: config.coerceTemporalFilterColumn,\n isExternalObject: config.isExternalObject,\n };\n\n // Build strategy chain (built-in + custom, sorted by priority)\n // InMemoryStrategy is NOT built-in — it lives in @objectstack/driver-memory\n // and should be passed via config.strategies when needed.\n // When fallbackService is configured, an internal delegate is added at P3.\n const builtIn: AnalyticsStrategy[] = [\n new NativeSQLStrategy(),\n new ObjectQLStrategy(),\n ];\n\n // Auto-add fallback delegate when fallbackService is provided\n if (config.fallbackService) {\n builtIn.push(new FallbackDelegateStrategy());\n }\n\n const custom = config.strategies || [];\n this.strategies = [...builtIn, ...custom].sort((a, b) => a.priority - b.priority);\n\n this.logger.info(\n `[Analytics] Initialized with ${this.cubeRegistry.size} cubes, ` +\n `${this.strategies.length} strategies: ${this.strategies.map(s => s.name).join(' → ')}`,\n );\n }\n\n /**\n * Build a per-call StrategyContext that binds the read-scope provider to the\n * current request's ExecutionContext (ADR-0021 D-C). The strategy then sees a\n * `getReadScope(objectName)` that already knows the active tenant.\n */\n private async callCtx(\n query: AnalyticsQuery,\n context?: ExecutionContext,\n ): Promise<StrategyContext> {\n // #3602 — `context` rides along unconditionally. It is the ENGINE-side belt\n // (forwarded to `engine.aggregate`, where the middleware chain applies its\n // own RLS), so it must not be gated on the analytics-side belt being wired:\n // a deployment with no `getReadScope` provider is exactly the one that most\n // needs the engine to scope for it.\n if (!this.readScopeProvider) return { ...this.baseCtx, context };\n // Pre-resolve the read scope for every object the strategy will scan (base\n // + all declared joins) BEFORE the synchronous SQL builder runs, since the\n // provider may be async (the production `security.getReadFilter` bridge).\n // The strategy then reads each object's filter synchronously from the map.\n const scopes = await this.resolveReadScopes(query, context);\n return {\n ...this.baseCtx,\n context,\n getReadScope: (objectName: string) => scopes.get(objectName) ?? null,\n };\n }\n\n /**\n * Resolve the read scope (tenant + RLS `FilterCondition`) for the base object\n * AND every joined object of the query's cube, keyed by object name. This is\n * the async pre-pass that lets the synchronous strategy enforce scoping even\n * when the provider (security `getReadFilter`) resolves asynchronously.\n *\n * The object set is `cube.sql` (base) plus every `cube.joins[*].name` — a\n * SUPERSET of what the strategy actually scans (the strategy only joins along\n * declared relationships), so no scanned object is ever left unscoped.\n *\n * Fail-closed: if the provider throws for an object, the whole query is\n * rejected rather than emitting SQL with that object unscoped.\n */\n private async resolveReadScopes(\n query: AnalyticsQuery,\n context?: ExecutionContext,\n ): Promise<Map<string, FilterCondition>> {\n const map = new Map<string, FilterCondition>();\n const provider = this.readScopeProvider;\n if (!provider || !query.cube) return map;\n const cube = this.cubeRegistry.get(query.cube);\n if (!cube) return map;\n\n const objects = new Set<string>();\n if (typeof cube.sql === 'string' && cube.sql.trim()) {\n objects.add(cube.sql.trim());\n }\n const joins = (cube as { joins?: Record<string, { name?: string }> }).joins;\n if (joins) {\n for (const [alias, j] of Object.entries(joins)) {\n objects.add(j?.name ?? alias);\n }\n }\n\n for (const object of objects) {\n let filter: FilterCondition | null | undefined;\n try {\n filter = await provider(object, context);\n } catch (e) {\n // Deny the entire query — never fall through to unscoped SQL.\n this.logger.error?.(\n `[Analytics] read-scope resolution failed for object \"${object}\" — ` +\n `rejecting query (fail-closed, ADR-0021 D-C)`,\n e instanceof Error ? e : new Error(String(e)),\n );\n throw new Error(\n `[Analytics] read-scope resolution failed for \"${object}\"; query denied (fail-closed).`,\n );\n }\n if (filter != null) map.set(object, filter);\n }\n return map;\n }\n\n /**\n * Execute an analytical query by delegating to the first capable strategy.\n *\n * A strategy can discover only AT EXECUTION TIME that the underlying driver\n * cannot serve it — the canonical case is NativeSQLStrategy on an in-memory\n * driver, whose `execute()` returns null for raw SQL (the auto-bridge throws\n * `RAW_SQL_UNSUPPORTED`). That is a capability miss, not a query error: fall\n * back to the next capable strategy (e.g. ObjectQLStrategy over the\n * aggregate bridge) instead of failing — or worse, fabricating empty rows.\n * Any other error propagates untouched.\n */\n async query(query: AnalyticsQuery, context?: ExecutionContext): Promise<AnalyticsResult> {\n if (!query.cube) {\n throw new Error('Cube name is required in analytics query');\n }\n\n this.ensureCube(query);\n const ctx = await this.callCtx(query, context);\n let skip: Set<AnalyticsStrategy> | undefined;\n for (;;) {\n const strategy = this.resolveStrategy(query, ctx, skip);\n this.logger.debug(`[Analytics] Query on cube \"${query.cube}\" → ${strategy.name}`);\n try {\n return await strategy.execute(query, ctx);\n } catch (e) {\n if ((e as { code?: string })?.code === 'RAW_SQL_UNSUPPORTED') {\n this.logger.warn(\n `[Analytics] ${strategy.name} cannot run on this driver (raw SQL unsupported) — falling back to the next strategy.`,\n );\n (skip ??= new Set()).add(strategy);\n continue;\n }\n throw e;\n }\n }\n }\n\n /**\n * Compile a `dataset` (ADR-0021) and register its Cube + join allowlist so it\n * can be queried by name. Idempotent (re-registering overwrites). Returns the\n * compiled dataset.\n */\n registerDataset(dataset: Dataset): CompiledDataset {\n const compiled = compileDataset(dataset, this.relationshipResolver);\n this.cubeRegistry.register(compiled.cube);\n this.datasetRegistry.set(dataset.name, compiled);\n return compiled;\n }\n\n /**\n * Execute a semantic-layer dataset (ADR-0021). Compiles the dataset (saved or\n * inline draft — Studio preview), registers its Cube + join allowlist, then\n * runs the selection through the `DatasetExecutor` with the request context so\n * tenant/RLS scoping (D-C) is applied. See {@link IAnalyticsService.queryDataset}.\n */\n async queryDataset(\n dataset: Dataset,\n selection: DatasetSelection,\n context?: ExecutionContext,\n options?: { previewDrafts?: boolean },\n ): Promise<AnalyticsResult> {\n const compiled = this.registerDataset(dataset);\n this.logger.debug(`[Analytics] queryDataset \"${dataset.name}\" (object=${dataset.object}, include=${(dataset.include ?? []).join(',') || '—'})`);\n\n // ── ADR-0037 P3 — draft data preview ────────────────────────────────────\n // When the request renders the as-if-published world AND the base object\n // has a PENDING seed draft, evaluate the selection over the seed's rows in\n // memory (a query-evaluating proxy feeds the unchanged DatasetExecutor, so\n // measure filters / compareTo / derived measures all behave identically).\n // No pending seed → fall through to the real engine: published objects\n // keep charting live data even inside a preview.\n if (options?.previewDrafts && this.draftRowsResolver) {\n let seedRows: Record<string, unknown>[] | null = null;\n try {\n seedRows = await this.draftRowsResolver(dataset.object, context);\n } catch (e) {\n this.logger.warn(`[Analytics] draft preview resolver failed for \"${dataset.object}\" — falling back to live data: ${String((e as Error)?.message ?? e)}`);\n }\n if (seedRows) {\n this.logger.debug(`[Analytics] queryDataset \"${dataset.name}\" → preview over ${seedRows.length} drafted seed row(s)`);\n const previewService = {\n query: async (q: AnalyticsQuery) => evaluateAnalyticsQueryOverRows(q, compiled.cube, seedRows!),\n } as IAnalyticsService;\n const previewResult = await new DatasetExecutor(previewService).execute(compiled, selection, context);\n // Label resolution is skipped on purpose: drafted seed rows reference\n // lookups by NAME (the seed convention), which already reads well.\n return previewResult;\n }\n }\n\n // #3602 — every label lookup in this request (sort keys below, display\n // labels further down) reads the REFERENCED object, so bind that object's\n // own read scope to this request once, up front.\n const provider = this.readScopeProvider;\n const resolveScope = provider\n ? (targetObject: string) => provider(targetObject, context)\n : undefined;\n // #3680 — per-request label-fetch cache. A selection that sorts by a\n // lookup dimension resolves labels twice (pre-window sort keys, then\n // post-window display); the cache makes the display pass reuse the ids the\n // sort already fetched, so label-ordering costs ONE id→name read total.\n const labelDeps = this.labelResolver ? withLabelFetchCache(this.labelResolver) : undefined;\n // #3680 — hand the executor the sort-key label hook so an `order` on a\n // select/lookup dimension sorts by the label the user reads. Built over\n // the SAME capabilities (and read scope) as the display resolution below.\n const orderLabels = labelDeps && dataset.dimensions?.length\n ? createOrderLabelResolver(\n dataset.object,\n dataset.dimensions\n .filter((d) => !!d.field)\n .map((d) => ({ name: d.name, field: d.field as string })),\n labelDeps,\n resolveScope,\n context,\n )\n : undefined;\n\n // Graceful degradation: a dashboard/report widget whose backing object or\n // table is not present in this kernel (e.g. a platform dashboard like\n // System Overview that charts `sys_audit_log`, opened in an environment\n // that never mounted the audit object) must render as \"no data\" — NOT\n // crash the widget with a 500. Datasets were the one read surface that\n // hard-failed on a missing source.\n let result: AnalyticsResult;\n try {\n result = await new DatasetExecutor(this, orderLabels).execute(compiled, selection, context);\n } catch (err) {\n if (isMissingSourceError(err)) {\n this.logger.warn(\n `[Analytics] dataset \"${dataset.name}\" backing object \"${dataset.object}\" is unavailable ` +\n `(${String((err as Error)?.message ?? err)}); returning an empty result instead of failing the widget`,\n );\n return { rows: [], fields: [], totals: [] };\n }\n throw err;\n }\n\n // Selected dimensions resolved against the dataset definition — shared by\n // drill metadata, label resolution, and dimension field-label enrichment.\n const selectedDims = (selection.dimensions ?? [])\n .map((name) => dataset.dimensions?.find((d) => d.name === name))\n .filter((d): d is NonNullable<typeof d> => !!d);\n\n // ADR-0021 D2 — drill-through metadata. A host (dashboard/report) drills a\n // clicked bucket back to the underlying records, but it only knows the\n // dimension NAMES, and the label resolution below OVERWRITES the raw grouped\n // value in each row with its display label. So before that happens, snapshot\n // the raw grouped values into a PARALLEL array (aligned to `rows` by index —\n // the result rows are NOT mutated) and expose the dataset's `object` +\n // dimension→field mapping so the renderer can build an exact-match filter.\n // Date buckets are excluded — a humanized bucket (\"2026-06\") can't be\n // exact-matched against the stored timestamp, so they are not drillable.\n const drillDims = selectedDims.filter((d) => !!d.field && d.type !== 'date');\n if (drillDims.length && result.rows.length) {\n (result as AnalyticsResultWithDrill).object = dataset.object;\n (result as AnalyticsResultWithDrill).dimensionFields = Object.fromEntries(\n drillDims.map((d) => [d.name, d.field as string]),\n );\n (result as AnalyticsResultWithDrill).drillRawRows = result.rows.map((row) => {\n const raw: Record<string, unknown> = {};\n for (const d of drillDims) raw[d.name] = row[d.name];\n return raw;\n });\n // #3214 — the totals/subtotal rows (#1753) carry dimension values too and\n // go through the SAME label resolution below, so snapshot their raw\n // grouped values here in the same pre-label pass. Aligned to `result.totals`\n // by index; each grouping is restricted to the drillable dims it actually\n // groups by (the grand-total grouping `[]` keeps empty maps, so a subtotal\n // drill filters by the stored value while the grand total drills unfiltered).\n if (result.totals?.length) {\n (result as AnalyticsResultWithDrill).drillRawTotals = result.totals.map((total) => {\n const groupingDims = drillDims.filter((d) => total.dimensions.includes(d.name));\n return total.rows.map((row) => {\n const raw: Record<string, unknown> = {};\n for (const d of groupingDims) raw[d.name] = row[d.name];\n return raw;\n });\n });\n }\n }\n\n // #1752 — date-range drill scope. A `dateGranularity` dimension groups a\n // SPAN of records into one bucket, so drilling it needs a half-open range\n // `[gte, lt)`, which the equality `drillRawRows` sidecar can't express\n // (that's exactly why date dims are excluded from `drillDims` above). Emit\n // a parallel range sidecar computed — via the shared inverse util so server\n // and client agree on boundaries — from the canonical bucket KEY, which is\n // still in `rows[i][dim]` here (this runs BEFORE label resolution rewrites\n // it to a display label).\n const rangeTz = selection.timezone ?? context?.timezone ?? 'UTC';\n // Per drillable date+granularity dim, decide how to serialize its bounds\n // (ADR-0053 temporal semantics):\n // - `datetime` → the reference tz's MIDNIGHT INSTANT (ISO), because the\n // bucket is defined on that tz's calendar (works under any tz, incl. DST);\n // - `date` → `YYYY-MM-DD` calendar bounds, a tz-naive calendar day that is\n // exact under ANY reference tz;\n // - unknown field type → safe only under UTC (where the calendar day and\n // its instant coincide); under a non-UTC tz we can't tell whether to\n // shift, so the dim is omitted and the host drills a superset.\n // The bucket size to invert MUST be the one the query actually grouped by —\n // `selection.dateGranularity` overrides the dataset dimension's default\n // (#3588). Reading `d.dateGranularity` here meant a widget that bucketed by\n // quarter or year got its ranges computed from the dataset's month (or,\n // when the dataset declared none, dropped entirely) — so drilling a bucket\n // opened the wrong span, or the chart lost drill-through altogether.\n const rangeDims: Array<{ d: (typeof selectedDims)[number]; granularity: DateGranularityValue; instant: boolean }> = [];\n for (const d of selectedDims) {\n if (!d.field || d.type !== 'date') continue;\n const granularity = resolveDimensionGranularity(selection, d.name, d.dateGranularity);\n if (!granularity) continue;\n const ftype = this.sourceFieldMeta?.(dataset.object, d.field as string)?.type;\n if (ftype === 'datetime') rangeDims.push({ d, granularity, instant: true });\n else if (ftype === 'date') rangeDims.push({ d, granularity, instant: false });\n else if (rangeTz === 'UTC') rangeDims.push({ d, granularity, instant: false });\n // else: unknown field type under a non-UTC reference tz → omit (superset).\n }\n if (rangeDims.length && result.rows.length) {\n const bound = (ymd: string, instant: boolean): string =>\n instant ? new Date(zonedDateStartToUtcMs(ymd, rangeTz)).toISOString() : ymd;\n (result as AnalyticsResultWithDrill).drillRanges = result.rows.map((row) => {\n const ranges: Record<string, { field: string; gte: string; lt: string }> = {};\n for (const { d, granularity, instant } of rangeDims) {\n // A row in the empty bucket carries `null` here (#3839) and yields no\n // range, so that row simply gets no drill bound — the superset.\n const cal = bucketKeyToCalendarRange(row[d.name] as string | null, granularity);\n if (cal) {\n ranges[d.name] = { field: d.field as string, gte: bound(cal.start, instant), lt: bound(cal.end, instant) };\n }\n }\n return ranges;\n });\n // The equality drill block sets `object` only when a NON-date drill dim\n // exists; a report grouped ONLY by time still needs the base object so the\n // host can open its list. Safe to (re)set to the same dataset object.\n (result as AnalyticsResultWithDrill).object = dataset.object;\n }\n\n // ADR-0021 — resolve grouped dimension values to human display labels\n // (select option label, lookup related-record name). Charts render the\n // dimension key verbatim, so this is the single place that turns a stored\n // value / FK id into the text a user expects to read.\n if (labelDeps && selectedDims.length) {\n // Same single-source rule as the drill ranges above: a date bucket must be\n // LABELLED with the granularity it was actually grouped by (#3588).\n // Formatting a `year` bucket with the dataset's `month` default rendered\n // it as \"1970-01\" — the year key re-parsed as an epoch millisecond count.\n const dims = selectedDims\n .filter((d) => !!d.field)\n .map((d) => ({\n name: d.name,\n field: d.field,\n type: d.type,\n dateGranularity: resolveDimensionGranularity(selection, d.name, d.dateGranularity),\n }));\n if (dims.length) {\n // `resolveScope` (hoisted above) binds the referenced object's read\n // scope to THIS request so the label lookup (a per-record read of the\n // related object) cannot surface a record the referenced object's RLS\n // would hide (#3602). `labelDeps` is the per-request cache over the\n // configured resolver, so ids the sort-key pass (#3680) already fetched\n // are not fetched again here.\n try {\n // `context` rides alongside `resolveScope` — the label lookup's SECOND\n // belt. `resolveScope` is this layer's own predicate; the context lets\n // the engine's middleware scope the same per-record read itself.\n await resolveDimensionLabels(dataset.object, dims, result.rows, labelDeps, resolveScope, context);\n // Totals rows (#1753) carry dimension values too (a row subtotal is\n // keyed by its row bucket) — resolve each grouping's own subset.\n for (const total of result.totals ?? []) {\n const subset = dims.filter((d) => total.dimensions.includes(d.name));\n if (subset.length) {\n await resolveDimensionLabels(dataset.object, subset, total.rows, labelDeps, resolveScope, context);\n }\n }\n } catch (e) {\n this.logger?.warn?.(`[Analytics] dimension label resolution failed for \"${dataset.name}\": ${String((e as Error)?.message ?? e)}`);\n }\n }\n }\n\n // ADR-0021 — enrich measure columns with their display `label` + `format`\n // so presentations show \"Tasks\" / \"$616,000\" instead of the raw measure\n // name \"task_count\" / \"616000\". Carried on the result fields; the renderer\n // applies the format (it can't be baked into the numeric row value).\n if (result.fields?.length && dataset.measures?.length) {\n const measureByName = new Map(dataset.measures.map((m) => [m.name, m]));\n for (const f of result.fields) {\n const m = measureByName.get(f.name) ?? measureByName.get(f.name.replace(/__compare$/, ''));\n if (!m) continue;\n if (f.label == null && typeof m.label === 'string') f.label = m.label;\n if (f.format == null && m.format) f.format = m.format;\n // ADR-0053 currency chain. A MONETARY measure resolves its display\n // currency from: explicit measure `currency` → source-field\n // `currencyConfig.defaultCurrency` → tenant default (`ctx.currency`). A\n // measure is monetary if it declares a currency OR aggregates a\n // `currency`-type field; non-monetary measures (count, avg of a plain\n // number) never receive a currency code.\n const fc = f as { currency?: string };\n const mc = m as { currency?: string };\n const meta = m.field ? this.sourceFieldMeta?.(dataset.object, m.field) : undefined;\n if (fc.currency == null) {\n const monetary = !!mc.currency || meta?.type === 'currency';\n if (monetary) {\n const resolved = mc.currency ?? meta?.defaultCurrency ?? context?.currency;\n if (resolved) fc.currency = resolved;\n }\n }\n // Percent scale chain (objectui#3136) — the currency chain's sibling.\n // A `%` format says how to PRINT a number, not what scale it is on, and\n // the two readings collide at exactly 1 (\"100%\" vs \"1%\"). Both answers\n // are in metadata, so answer here instead of leaving the renderer to\n // guess from the value's magnitude: a `ratio` is a 0–1 fraction by\n // definition, and any aggregate of a `percent` field (avg/min/max —\n // sum is already rejected as incoherent) keeps that field's own scale.\n if (f.percentScale == null) {\n f.percentScale = m.derived?.op === 'ratio' ? 'fraction' : percentScaleOf(meta);\n }\n }\n }\n\n // Enrich DIMENSION columns with their display `label` too, so a grouped\n // table header reads \"Status\" instead of the raw field name \"status\". The\n // measure-only enrichment above left dimension headers bare (the renderer\n // then fell back to the raw dimension name).\n if (result.fields?.length && selectedDims.length) {\n const dimByName = new Map(selectedDims.map((d) => [d.name, d]));\n const dimByField = new Map(selectedDims.filter((d) => !!d.field).map((d) => [d.field as string, d]));\n for (const f of result.fields) {\n if (f.label != null) continue;\n // Result fields may be keyed by the dataset dimension NAME or the\n // underlying cube FIELD depending on strategy — match either.\n const d = dimByName.get(f.name) ?? dimByField.get(f.name);\n if (d && typeof d.label === 'string') f.label = d.label;\n }\n }\n return result;\n }\n\n /**\n * Get cube metadata for discovery.\n */\n async getMeta(cubeName?: string): Promise<CubeMeta[]> {\n // If a fallback service is configured, merge its metadata with the registry\n const cubes = cubeName\n ? [this.cubeRegistry.get(cubeName)].filter(Boolean) as Cube[]\n : this.cubeRegistry.getAll();\n\n return cubes.map(cube => ({\n name: cube.name,\n title: cube.title,\n measures: Object.entries(cube.measures).map(([key, measure]) => ({\n name: `${cube.name}.${key}`,\n type: measure.type,\n title: measure.label,\n })),\n dimensions: Object.entries(cube.dimensions).map(([key, dimension]) => ({\n name: `${cube.name}.${key}`,\n type: dimension.type,\n title: dimension.label,\n })),\n }));\n }\n\n /**\n * Generate SQL for a query without executing it (dry-run).\n */\n async generateSql(query: AnalyticsQuery, context?: ExecutionContext): Promise<{ sql: string; params: unknown[] }> {\n if (!query.cube) {\n throw new Error('Cube name is required for SQL generation');\n }\n\n this.ensureCube(query);\n const ctx = await this.callCtx(query, context);\n const strategy = this.resolveStrategy(query, ctx);\n this.logger.debug(`[Analytics] generateSql on cube \"${query.cube}\" → ${strategy.name}`);\n\n return strategy.generateSql(query, ctx);\n }\n\n // ── Internal ─────────────────────────────────────────────────────\n\n /**\n * Ensure a cube exists for the given query and that it knows about every\n * measure referenced by the query.\n *\n * - If no cube is registered for `query.cube`, infer a minimal cube from\n * the query so downstream strategies (which assume `cube.sql` exists)\n * don't crash.\n * - If a cube exists but the query references measures that aren't in\n * `cube.measures` (e.g. `amount_sum`, `amount_avg` emitted by dashboard\n * widget translators), inject suffix-inferred Metric entries so the\n * strategies pick the right aggregation function and field.\n */\n private ensureCube(query: AnalyticsQuery): void {\n const name = query.cube!;\n let cube = this.cubeRegistry.get(name);\n\n if (!cube) {\n // [#3867] Auto-inference below sets `cube.sql = name`, so from here on\n // the queried string IS a physical table name. Verify it names a\n // registered object BEFORE that happens — otherwise `/analytics/query`\n // is a way to aggregate over any table the connection can see, exactly\n // the hole #3770 closed on the data path. A registered Cube needs no\n // such check: it was authored, and its `sql` is whatever it declares.\n this.assertInferableCube(name);\n cube = this.inferCubeFromQuery(query);\n // [#4437] Validate the inferred measures' SOURCE FIELDS before the cube\n // is registered — a rejected query must leave no trace in the registry\n // (same rule the #3867 gate above keeps), or a retry would find a\n // \"registered\" cube carrying the bogus measure and sail straight to SQL.\n this.assertMeasureFields(query, cube, Object.keys(cube.measures));\n this.cubeRegistry.register(cube);\n // A scalar query — only measures, no grouping (no `dimensions`/\n // `timeDimensions`) — is the first-class \"metric over an object\" path\n // (e.g. the `object-metric` KPI widget). Auto-inferring a count/sum cube\n // is the intended behaviour there, so log at debug. A query that groups\n // by an explicit dimension or time bucket almost certainly meant to hit a\n // registered cube; keep that at warn so a forgotten registration is loud.\n const isScalarMetric =\n (query.dimensions?.length ?? 0) === 0 && (query.timeDimensions?.length ?? 0) === 0;\n const message =\n `[Analytics] No cube registered for \"${name}\"; auto-inferred a minimal cube ` +\n `(sql=\"${name}\", measures=${Object.keys(cube.measures).join(',') || '(none)'}, ` +\n `dimensions=${Object.keys(cube.dimensions).join(',') || '(none)'}). ` +\n `Define an explicit Cube in your stack for full control.`;\n if (isScalarMetric) this.logger.debug(message);\n else this.logger.warn(message);\n return;\n }\n\n // Cube exists — check for unknown measures referenced by the query and\n // augment the cube with suffix-inferred Metric definitions so callers\n // that pass `<field>_sum` / `<field>_avg` etc. get the right aggregation.\n const stripPrefix = (m: string) => (m.includes('.') ? m.split('.').slice(1).join('.') : m);\n const extraMeasures: Record<string, any> = {};\n for (const m of query.measures || []) {\n const key = stripPrefix(m);\n if (cube.measures[key] || extraMeasures[key]) continue;\n extraMeasures[key] = inferMeasure(key);\n }\n if (Object.keys(extraMeasures).length > 0) {\n const augmented: Cube = {\n ...cube,\n measures: { ...cube.measures, ...extraMeasures },\n };\n // [#4437] The cube's DECLARED measures are the ones a caller may name;\n // the suffix-inferred entries just added are a convenience, not a\n // vocabulary. Snapshot the declared list BEFORE registering the augmented\n // cube so the rejection can suggest what the caller could have meant —\n // and so a rejected query leaves the registry as it found it.\n this.assertMeasureFields(query, augmented, Object.keys(cube.measures));\n this.cubeRegistry.register(augmented);\n this.logger.debug(\n `[Analytics] Augmented cube \"${name}\" with inferred measures: ${Object.keys(extraMeasures).join(',')}`,\n );\n } else {\n // No inference happened — every measure is declared. Still validate: an\n // authored cube can declare a measure over a field the object dropped.\n this.assertMeasureFields(query, cube, Object.keys(cube.measures));\n }\n }\n\n /**\n * [#4437] Reject a measure whose SOURCE FIELD the backing object does not\n * have, BEFORE the strategy compiles it into SQL.\n *\n * `inferMeasure` maps a suffix convention onto a field name and has no way to\n * know whether that field exists: `ghost_sum` happily became `SUM(ghost)`, the\n * driver threw `no such column`, and the caller got\n * `500 {\"code\":\"SQLITE_ERROR\",\"message\":\"Internal server error\"}` — a driver\n * error class on the wire, and nothing actionable, for what is a plain typo.\n * The DATA route has refused the same mistake with a `400 INVALID_FIELD`\n * naming the field since #4315/#4254; this is the analytics half of that\n * answer, and it is deliberately the SAME envelope (`code`/`field`/`object`/\n * `param`) so one mistake has one shape across both routes.\n *\n * What it checks, and what it deliberately does not:\n *\n * - Only when the cube's `sql` is a bare OBJECT NAME. An authored cube whose\n * `sql` is a real SQL expression has no field list to check against.\n * - Only when {@link AnalyticsServiceConfig.getObjectFieldNames} answers.\n * Absent hook / unknown object → stand down (see the config field's doc).\n * - Only measures whose source is a BARE COLUMN. `count(*)` has no source\n * field, and a dotted reference (`account.industry`) resolves through a\n * join whose target this check cannot see — both pass through untouched.\n * - `id` / `created_at` / `updated_at` are admitted unconditionally, matching\n * the data path's `resolveQueryFields`: they are engine-assigned rather than\n * declared, and a gate stricter than the engine it guards would reject\n * queries that used to work.\n */\n private assertMeasureFields(query: AnalyticsQuery, cube: Cube, declaredMeasures: string[]): void {\n const probe = this.getObjectFieldNames;\n if (!probe) return;\n const measures = query.measures ?? [];\n if (measures.length === 0) return;\n\n const object = typeof cube.sql === 'string' ? cube.sql.trim() : '';\n if (!object || !BARE_IDENTIFIER.test(object)) return;\n const fieldNames = probe(object);\n if (!fieldNames || fieldNames.length === 0) return;\n const known = new Set<string>([...fieldNames, 'id', 'created_at', 'updated_at']);\n\n const stripPrefix = (m: string) => (m.includes('.') ? m.split('.').slice(1).join('.') : m);\n /** The source field a measure aggregates, or null when there is nothing to check. */\n const sourceFieldOf = (measure: string): string | null => {\n const metric = cube.measures[stripPrefix(measure)] as { type?: string; sql?: unknown } | undefined;\n if (!metric) return null;\n // `count(*)` is the one legitimately field-less aggregate.\n if (metric.type === 'count' && (metric.sql === '*' || metric.sql == null)) return null;\n const source = typeof metric.sql === 'string' ? metric.sql.trim() : '';\n if (!source || source === '*' || !BARE_IDENTIFIER.test(source)) return null;\n return source;\n };\n\n // Two passes so the rejection can suggest the measures that WOULD have\n // worked. On the auto-inference path `cube.measures` already carries the\n // caller's own bogus spelling (it was inferred from the query moments ago),\n // so echoing the cube's measure list verbatim would offer the typo back as\n // a valid alternative — the one suggestion guaranteed to be wrong.\n const invalid = new Set<string>();\n for (const measure of measures) {\n const source = sourceFieldOf(measure);\n if (source && !known.has(source)) invalid.add(stripPrefix(measure));\n }\n if (invalid.size === 0) return;\n const usable = declaredMeasures.filter((m) => !invalid.has(m));\n\n for (const measure of measures) {\n const source = sourceFieldOf(measure);\n if (!source || known.has(source)) continue;\n\n const err = new Error(\n `Measure '${measure}' on cube '${cube.name}' aggregates field '${source}', which object ` +\n `'${object}' does not have. ` +\n `Valid measures: ${usable.join(', ') || '(none)'}. ` +\n `Other measures are inferred from the object's OWN fields as ` +\n `'<field>_sum' / '_avg' / '_min' / '_max' / '_count_distinct', so check the spelling of ` +\n `'${source}' — known fields: ${[...fieldNames].sort().join(', ')}.`,\n ) as Error & { code?: string; status?: number; field?: string; object?: string; param?: string; measure?: string };\n err.code = 'INVALID_FIELD';\n err.status = 400;\n err.field = source;\n err.object = object;\n err.param = 'measures';\n err.measure = measure;\n throw err;\n }\n }\n\n /**\n * [#3867] Gate on the cube auto-inference path: a name with no registered\n * Cube may only be inferred into one if it is a registered object.\n *\n * Rejects with `status: 404` / `code: 'CUBE_NOT_FOUND'` so the HTTP boundary\n * answers \"no such cube\" instead of letting the name reach the driver as a\n * table and surfacing whatever the driver says about it. The message names\n * both ways the request could be made valid, because from here the two are\n * genuinely indistinguishable: register a Cube, or register the object.\n *\n * Skips when `isRegisteredObject` was not supplied — see the config field's\n * doc for why that tier is a deliberate stand-down and not a hole.\n */\n private assertInferableCube(name: string): void {\n const isRegisteredObject = this.isRegisteredObject;\n if (!isRegisteredObject) {\n if (!this.warnedNoObjectRegistry) {\n this.warnedNoObjectRegistry = true;\n this.logger.warn(\n '[Analytics] no object-registry hook configured — the cube-inference existence gate ' +\n '(#3867) is INACTIVE for this service; an unregistered cube name reaches the driver ' +\n 'as a raw table name.',\n );\n }\n return;\n }\n if (isRegisteredObject(name)) return;\n const err = new Error(\n `Cube '${name}' not found: no cube is registered under that name, and it is not a ` +\n `registered object either (a cube can only be auto-inferred from a registered object). ` +\n `Define a Cube in your stack, or check the object name.`,\n ) as Error & { code?: string; status?: number; cube?: string };\n err.code = 'CUBE_NOT_FOUND';\n err.status = 404;\n err.cube = name;\n throw err;\n }\n\n /** Build a minimal Cube from the fields referenced by an AnalyticsQuery. */\n private inferCubeFromQuery(query: AnalyticsQuery): Cube {\n const cubeName = query.cube!;\n const measures: Record<string, any> = {};\n const dimensions: Record<string, any> = {};\n\n const stripPrefix = (m: string) => (m.includes('.') ? m.split('.').slice(1).join('.') : m);\n\n // Always provide a default `count` measure\n measures.count = { name: 'count', label: 'Count', type: 'count', sql: '*' };\n\n for (const m of query.measures || []) {\n const key = stripPrefix(m);\n if (measures[key]) continue;\n const inferred = inferMeasure(key);\n measures[key] = inferred;\n }\n\n for (const d of query.dimensions || []) {\n const key = stripPrefix(d);\n if (dimensions[key]) continue;\n dimensions[key] = { name: key, label: key, type: 'string', sql: key };\n }\n\n if (query.where && typeof query.where === 'object' && !Array.isArray(query.where)) {\n // Canonical FilterCondition: top-level keys (excluding logical\n // combinators) are field names. We only need them to seed an\n // ad-hoc cube definition for free-form queries.\n for (const key of Object.keys(query.where as Record<string, unknown>)) {\n if (key.startsWith('$')) continue;\n const stripped = stripPrefix(key);\n if (dimensions[stripped] || measures[stripped]) continue;\n dimensions[stripped] = { name: stripped, label: stripped, type: 'string', sql: stripped };\n }\n }\n\n for (const td of query.timeDimensions || []) {\n const key = stripPrefix(td.dimension);\n if (dimensions[key]) continue;\n dimensions[key] = {\n name: key, label: key, type: 'time', sql: key,\n granularities: ['day', 'week', 'month', 'quarter', 'year'],\n };\n }\n\n return {\n name: cubeName,\n title: cubeName,\n sql: cubeName,\n measures,\n dimensions,\n public: false,\n };\n }\n\n /**\n * Walk the strategy chain and return the first strategy that can handle the\n * query. `skip` excludes strategies that already proved incapable at\n * execution time (see {@link query}'s RAW_SQL_UNSUPPORTED fallback).\n */\n private resolveStrategy(\n query: AnalyticsQuery,\n ctx: StrategyContext,\n skip?: Set<AnalyticsStrategy>,\n ): AnalyticsStrategy {\n for (const strategy of this.strategies) {\n if (skip?.has(strategy)) continue;\n if (strategy.canHandle(query, ctx)) {\n return strategy;\n }\n }\n throw new Error(\n `[Analytics] No strategy can handle query for cube \"${query.cube}\". ` +\n `Checked: ${this.strategies.map(s => s.name).join(', ')}${skip?.size ? ` (skipped at runtime: ${[...skip].map((s) => s.name).join(', ')})` : ''}. ` +\n 'Ensure a compatible driver is configured or a fallback service is registered.',\n );\n }\n}\n\n/**\n * Infer a Metric definition from a measure key name.\n *\n * Recognised suffix conventions (matches dashboard widget translators that\n * emit measures like `<field>_sum`, `<field>_avg`):\n *\n * | Suffix | Aggregation |\n * |:-------------------|:----------------|\n * | `count` | `count(*)` |\n * | `_sum` | `sum(field)` |\n * | `_avg` / `_average`| `avg(field)` |\n * | `_min` | `min(field)` |\n * | `_max` | `max(field)` |\n * | `_count_distinct` | `count(distinct field)` |\n *\n * Anything else is treated as a `sum(<key>)` — best-effort default for an\n * unknown numeric measure.\n */\nexport function inferMeasure(key: string): { name: string; label: string; type: 'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'; sql: string } {\n if (key === 'count') {\n return { name: 'count', label: 'Count', type: 'count', sql: '*' };\n }\n const suffixes: Array<[string, 'sum' | 'avg' | 'min' | 'max' | 'count_distinct']> = [\n ['_count_distinct', 'count_distinct'],\n ['_sum', 'sum'],\n ['_avg', 'avg'],\n ['_average', 'avg'],\n ['_min', 'min'],\n ['_max', 'max'],\n ];\n for (const [suffix, type] of suffixes) {\n if (key.endsWith(suffix)) {\n const field = key.slice(0, -suffix.length) || '*';\n return { name: key, label: key, type, sql: field };\n }\n }\n return { name: key, label: key, type: 'sum', sql: key };\n}\n\n/**\n * FallbackDelegateStrategy — Internal strategy for fallback service delegation.\n *\n * Automatically added to the strategy chain when `fallbackService` is configured.\n * Not exported — consumers who need explicit in-memory support should use\n * `InMemoryStrategy` from `@objectstack/driver-memory`.\n */\nclass FallbackDelegateStrategy implements AnalyticsStrategy {\n readonly name = 'FallbackDelegateStrategy';\n readonly priority = 30;\n\n canHandle(query: AnalyticsQuery, ctx: StrategyContext): boolean {\n if (!query.cube) return false;\n return !!ctx.fallbackService;\n }\n\n async execute(query: AnalyticsQuery, ctx: StrategyContext): Promise<AnalyticsResult> {\n return ctx.fallbackService!.query(query);\n }\n\n async generateSql(query: AnalyticsQuery, ctx: StrategyContext): Promise<{ sql: string; params: unknown[] }> {\n if (ctx.fallbackService?.generateSql) {\n return ctx.fallbackService.generateSql(query);\n }\n return {\n sql: `-- FallbackDelegateStrategy: SQL generation not supported for cube \"${query.cube}\"`,\n params: [],\n };\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Cube } from '@objectstack/spec/data';\n\n/**\n * CubeRegistry — Central registry for analytics cube definitions.\n *\n * Cubes can be registered from two sources:\n * 1. **Manifest definitions** — Explicit cube definitions in `objectstack.config.ts`.\n * 2. **Object schema inference** — Auto-generated cubes from ObjectQL object schemas.\n *\n * The registry is the single source of truth for cube metadata discovery\n * (used by `getMeta()` and the strategy chain).\n */\nexport class CubeRegistry {\n private cubes = new Map<string, Cube>();\n\n /** Register a single cube definition. Overwrites if name already exists. */\n register(cube: Cube): void {\n this.cubes.set(cube.name, cube);\n }\n\n /** Register multiple cube definitions at once. */\n registerAll(cubes: Cube[]): void {\n for (const cube of cubes) {\n this.register(cube);\n }\n }\n\n /** Get a cube definition by name. */\n get(name: string): Cube | undefined {\n return this.cubes.get(name);\n }\n\n /** Check if a cube is registered. */\n has(name: string): boolean {\n return this.cubes.has(name);\n }\n\n /** Return all registered cubes. */\n getAll(): Cube[] {\n return Array.from(this.cubes.values());\n }\n\n /** Return all cube names. */\n names(): string[] {\n return Array.from(this.cubes.keys());\n }\n\n /** Number of registered cubes. */\n get size(): number {\n return this.cubes.size;\n }\n\n /** Remove all cubes. */\n clear(): void {\n this.cubes.clear();\n }\n\n /**\n * Auto-generate a cube definition from an object schema.\n *\n * Heuristic rules:\n * - `number` fields → `sum`, `avg`, `min`, `max` measures\n * - `boolean` fields → `count` measure (count where true)\n * - All non-computed fields → dimensions\n * - `date`/`datetime` fields → time dimensions with standard granularities\n * - A default `count` measure is always added\n *\n * @param objectName - The snake_case object name (used as table/cube name)\n * @param fields - Array of field descriptors `{ name, type, label? }`\n */\n inferFromObject(\n objectName: string,\n fields: Array<{ name: string; type: string; label?: string }>,\n ): Cube {\n const measures: Record<string, any> = {\n count: {\n name: 'count',\n label: 'Count',\n type: 'count',\n sql: '*',\n },\n };\n const dimensions: Record<string, any> = {};\n\n for (const field of fields) {\n const label = field.label || field.name;\n\n // All fields become dimensions\n const dimType = this.fieldTypeToDimensionType(field.type);\n dimensions[field.name] = {\n name: field.name,\n label,\n type: dimType,\n sql: field.name,\n ...(dimType === 'time'\n ? { granularities: ['day', 'week', 'month', 'quarter', 'year'] }\n : {}),\n };\n\n // Numeric fields also become aggregation measures\n if (field.type === 'number' || field.type === 'currency' || field.type === 'percent') {\n measures[`${field.name}_sum`] = {\n name: `${field.name}_sum`,\n label: `${label} (Sum)`,\n type: 'sum',\n sql: field.name,\n };\n measures[`${field.name}_avg`] = {\n name: `${field.name}_avg`,\n label: `${label} (Avg)`,\n type: 'avg',\n sql: field.name,\n };\n }\n }\n\n const cube: Cube = {\n name: objectName,\n title: objectName,\n sql: objectName,\n measures,\n dimensions,\n public: false,\n };\n\n this.register(cube);\n return cube;\n }\n\n private fieldTypeToDimensionType(fieldType: string): string {\n switch (fieldType) {\n case 'number':\n case 'currency':\n case 'percent':\n return 'number';\n case 'boolean':\n return 'boolean';\n case 'date':\n case 'datetime':\n return 'time';\n default:\n return 'string';\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Filter Normalization for the Analytics Layer\n *\n * The analytics endpoint accepts filters via the canonical `where`\n * field per the unified Query DSL (`spec/data/query.zod.ts`):\n *\n * - MongoDB-style FilterCondition: `{ field: value }` /\n * `{ field: { $op: value } }` / `{ $and: [...] }` — defined in\n * `spec/data/filter.zod.ts` and used by `find()`, dashboard\n * widget `filter`, RLS, etc.\n *\n * `normalizeAnalyticsFilters` flattens the FilterCondition tree into\n * the internal array form used by the SQL/Mongo pipeline strategies.\n * Strategies stay simple — they only need to know one shape — and the\n * spec is honoured: dashboard metadata is authored once in the\n * canonical MongoDB form and the server normalizes at the boundary.\n *\n * # Coverage — a dropped predicate WIDENS the query, so nothing is dropped\n *\n * Failing to map an operator is not \"not supporting\" it: the predicate simply\n * disappears, the compiled SQL stays valid, and the query returns rows the\n * author excluded. It reads as a chart drawn over the whole dataset (#3650's\n * symptom) and is invisible to any test that asserts the emitted SQL string.\n * `$between`, `$startsWith`, `$endsWith` and `$null` each sat broken that way\n * (#4128), so what this maps is now a complete capability claim over\n * `filter.zod.ts`'s authorable vocabulary:\n *\n * - mapped 1:1 — `$eq` `$ne` `$gt` `$gte` `$lt` `$lte` `$in` `$nin`\n * `$contains` `$notContains` `$startsWith` `$endsWith`;\n * - value-DEPENDENT, so resolved explicitly rather than through the map —\n * `$null` and `$exists`, whose meaning flips with their boolean;\n * - lowered — `$between`, which becomes its two bounds so each strategy's\n * existing upper-bound handling applies the calendar-day whole-day rule\n * (see the note at the lowering);\n * - structural — `$and` / `$or` / `$not`, carried as tree nodes;\n * - anything else THROWS. An operator outside the vocabulary is a caller\n * error, and a loud one beats a silently widened read — the call\n * driver-memory made for the same shape in #3948.\n *\n * `$or` / `$not` were the last of that family, and they were dropped for a\n * structural reason rather than an oversight: this module produced a flat\n * ARRAY, which cannot carry a disjunction. So an author's `{$or: […]}`\n * vanished from the WHERE clause and the widget drew every row. The output is\n * now a {@link NormalizedFilterNode} tree, and each strategy compiles it the\n * way its own backend expresses a disjunction.\n *\n * Row-result cover: `filter-operator-coverage.test.ts` for the operator\n * vocabulary, and `native-sql-filter-logic-conformance.test.ts`, which runs\n * the SHARED combinator table (`FILTER_LOGIC_CASES`, #3774) that the SQL\n * compiler, the in-memory matcher, `formula` and `read-scope-sql` are already\n * held to.\n */\n\nexport interface NormalizedAnalyticsFilter {\n member: string;\n operator: string;\n values: string[];\n}\n\n/**\n * The value-INDEPENDENT operators: the pipeline name depends only on the key.\n *\n * `$null` and `$exists` are deliberately absent — their meaning flips with\n * their boolean value, which a key→name map cannot express. Putting `$exists`\n * here anyway is what made `{$exists: false}` compile to `IS NOT NULL`, the\n * exact inverse of what it asks for; both are handled explicitly below.\n */\nconst MONGO_TO_CUBE_OP: Record<string, string> = {\n $eq: 'equals',\n $ne: 'notEquals',\n $gt: 'gt',\n $gte: 'gte',\n $lt: 'lt',\n $lte: 'lte',\n $in: 'in',\n $nin: 'notIn',\n $contains: 'contains',\n $notContains: 'notContains',\n $startsWith: 'startsWith',\n $endsWith: 'endsWith',\n};\n\n/**\n * Stringify a filter value as the internal pipeline requires `values: string[]`.\n *\n * Booleans serialize as the tokens `'true'`/`'false'` (NOT `'1'`/`'0'`) so the\n * boolean identity survives the string roundtrip: the consuming strategies can\n * recover a real boolean for the ObjectQL engine (which compares against the\n * stored boolean type) while still binding `1`/`0` for SQL. Stringifying to\n * `'1'`/`'0'` was indistinguishable from a numeric 1/0 and made every boolean\n * equality filter / boolean group-by compare a number against a boolean — and\n * never match.\n */\nfunction stringifyForCube(v: unknown): string {\n if (v == null) return '';\n if (typeof v === 'boolean') return v ? 'true' : 'false';\n if (v instanceof Date) return v.toISOString();\n if (typeof v === 'object') return JSON.stringify(v);\n return String(v);\n}\n\n/**\n * One node of the normalized filter TREE.\n *\n * A tree rather than the flat array this module used to produce, because a flat\n * array cannot express `$or` — and what it did with one was DROP it, which does\n * not narrow a query, it widens it to rows the author excluded (#3650's\n * symptom). The structure is the minimum that survives that: leaves carry the\n * pipeline's `{member, operator, values}` triple unchanged, and the combinators\n * are explicit so each strategy can compile them the way its own backend\n * expresses them — recursive SQL for the raw-SQL path, a passed-through\n * `$or`/`$not` for the engine path.\n */\nexport type NormalizedFilterNode =\n | { kind: 'leaf'; member: string; operator: string; values: string[] }\n | { kind: 'and'; children: NormalizedFilterNode[] }\n | { kind: 'or'; children: NormalizedFilterNode[] }\n | { kind: 'not'; child: NormalizedFilterNode };\n\n/** `null` means \"no constraint\" — an empty object contributes no predicate. */\nfunction andOf(children: NormalizedFilterNode[]): NormalizedFilterNode | null {\n if (children.length === 0) return null;\n if (children.length === 1) return children[0];\n return { kind: 'and', children };\n}\n\n/**\n * Compile one `field: value | { $op: … }` entry into its leaves.\n *\n * Multiple operators on one field AND together — the rule\n * `FILTER_LOGIC_CASES` pins for every other backend, and the one a range\n * `{ $gte, $lte }` depends on.\n */\nfunction fieldLeaves(key: string, raw: unknown): NormalizedFilterNode[] {\n const out: NormalizedFilterNode[] = [];\n const leaf = (operator: string, values: string[]): void => {\n out.push({ kind: 'leaf', member: key, operator, values });\n };\n\n if (raw === null) {\n leaf('notSet', []);\n return out;\n }\n\n if (typeof raw === 'object' && !Array.isArray(raw) && !(raw instanceof Date)) {\n const wrapper = raw as Record<string, unknown>;\n const opKeys = Object.keys(wrapper).filter((k) => k.startsWith('$'));\n if (opKeys.length > 0) {\n for (const opKey of opKeys) {\n // `$between [min, max]` LOWERS to its two bounds rather than getting a\n // `between` operator of its own. Both strategies already carry the\n // calendar-day whole-day rule on their upper bound — NativeSQLStrategy\n // compiles a bare-day `lte` half-open (#3777), ObjectQLStrategy hands\n // `$lte` to the driver, which does the same — so a range's max\n // inherits that rule by construction instead of needing a second\n // implementation to keep in step. (The preview evaluator's `$between`\n // gap was closed the same way, sharing its `$lte` helper.)\n //\n // Before this, `$between` was simply absent from the operator map and\n // fell to the `continue` below: the predicate VANISHED from the WHERE\n // clause, so a dashboard widget carrying a range filter charted the\n // entire dataset — #3650's symptom, on the surface #3650 was about.\n // The temporal conformance matrix caught it as row results\n // (`native-sql-temporal-conformance.test.ts`).\n if (opKey === '$between') {\n const v = wrapper[opKey];\n if (!Array.isArray(v) || v.length !== 2) {\n // Never drop it: an unbounded read is the failure mode this whole\n // branch exists to prevent, and it is indistinguishable from a\n // legitimately wide query. Same stance driver-memory took for the\n // same shape (#3948).\n throw new Error(\n `[analytics] \"$between\" on \"${key}\" needs a two-element [min, max] array, got ` +\n `${JSON.stringify(v)}. Dropping the predicate would silently widen the query to every row.`,\n );\n }\n leaf('gte', [stringifyForCube(v[0])]);\n leaf('lte', [stringifyForCube(v[1])]);\n continue;\n }\n\n // The two null predicates read their BOOLEAN, not just their key —\n // which is why neither can live in MONGO_TO_CUBE_OP. `$null: true`\n // asks for IS NULL (`notSet`), `$null: false` for IS NOT NULL\n // (`set`); `$exists` is the mirror image. `$null` is the shape the\n // console emits for an \"is empty\" / \"is not empty\" filter\n // (`is_null`/`is_not_null` normalise to it in `filter.zod.ts`), so\n // dropping it silently meant such a widget showed every row.\n if (opKey === '$null' || opKey === '$exists') {\n const isNull = opKey === '$null' ? wrapper[opKey] === true : wrapper[opKey] === false;\n leaf(isNull ? 'notSet' : 'set', []);\n continue;\n }\n\n const cubeOp = MONGO_TO_CUBE_OP[opKey];\n if (!cubeOp) {\n // NEVER drop: a missing predicate does not narrow the query, it\n // WIDENS it — the compiled SQL stays valid and simply returns rows\n // the author excluded, which is indistinguishable from a\n // legitimately broad query and invisible to any test that asserts\n // the emitted SQL. That failure mode is #3650's, and skipping\n // unmapped operators is how `$between` reproduced it (#4128).\n // driver-memory made the same call for the same reason in #3948.\n throw new Error(\n `[analytics] Unsupported filter operator \"${opKey}\" on \"${key}\". ` +\n `Supported: ${Object.keys(MONGO_TO_CUBE_OP).join(', ')}, $between, $null, $exists, ` +\n `and the $and/$or/$not combinators. ` +\n `Dropping it would silently widen the query to rows the filter excludes.`,\n );\n }\n const v = wrapper[opKey];\n leaf(cubeOp, Array.isArray(v) ? v.map(stringifyForCube) : [stringifyForCube(v)]);\n }\n return out;\n }\n // Nested relation (e.g. {profile: {verified: true}}). Flatten with\n // dot-prefixed keys so cube field path resolution still works.\n for (const [nestedKey, nestedVal] of Object.entries(wrapper)) {\n out.push(...fieldLeaves(`${key}.${nestedKey}`, nestedVal));\n }\n return out;\n }\n\n // Implicit equality / array → in\n if (Array.isArray(raw)) leaf('in', raw.map(stringifyForCube));\n else leaf('equals', [stringifyForCube(raw)]);\n return out;\n}\n\n/**\n * Compile a `FilterCondition` object into a node. `null` = no constraint.\n *\n * Every entry of one object ANDs with its siblings, at every depth — the rule\n * `filter-logic-conformance.ts` exists to hold each backend to (#3774). The\n * combinator handling deliberately mirrors `read-scope-sql.ts`'s\n * `compileNode`, including its fail-closed empty-array rejection, so the two\n * SQL-producing paths in this package cannot drift apart about what a filter\n * MEANS.\n */\nfunction buildNode(cond: Record<string, unknown>): NormalizedFilterNode | null {\n const children: NormalizedFilterNode[] = [];\n\n for (const [key, raw] of Object.entries(cond)) {\n if (raw === undefined) continue;\n\n if (key === '$and' || key === '$or') {\n if (!Array.isArray(raw) || raw.length === 0) {\n throw new Error(\n `[analytics] \"${key}\" requires a non-empty array. An empty combinator has no ` +\n `defensible reading — dropping it widens the query, and treating it as \"match ` +\n `nothing\" silently empties a chart.`,\n );\n }\n const branches = raw\n .map((sub) => (sub && typeof sub === 'object' ? buildNode(sub as Record<string, unknown>) : null))\n .filter((n): n is NormalizedFilterNode => n !== null);\n if (branches.length === 0) continue;\n // `$and` folds into this object's own AND; `$or` becomes a node, since\n // OR is exactly the structure a flat list could not carry.\n if (key === '$and') children.push(...branches);\n else children.push(branches.length === 1 ? branches[0] : { kind: 'or', children: branches });\n continue;\n }\n\n if (key === '$not') {\n const inner = raw && typeof raw === 'object' ? buildNode(raw as Record<string, unknown>) : null;\n if (inner) children.push({ kind: 'not', child: inner });\n continue;\n }\n\n if (key.startsWith('$')) {\n throw new Error(\n `[analytics] Unsupported top-level filter operator \"${key}\". ` +\n `Dropping it would silently widen the query to rows the filter excludes.`,\n );\n }\n\n children.push(...fieldLeaves(key, raw));\n }\n\n return andOf(children);\n}\n\n/**\n * Normalize an analytics query's `where` (FilterCondition) into the tree the\n * strategies compile. `null` when the query carries no `where`.\n */\nexport function normalizeAnalyticsFilterTree(\n query: { where?: unknown } | unknown,\n): NormalizedFilterNode | null {\n if (!query || typeof query !== 'object') return null;\n const where = (query as { where?: unknown }).where;\n if (!where || typeof where !== 'object' || Array.isArray(where)) return null;\n return buildNode(where as Record<string, unknown>);\n}\n\n/**\n * Every leaf in the tree, structure discarded.\n *\n * For asking WHICH MEMBERS a filter touches — the cross-object envelope check\n * is the caller. Never for building a predicate: the leaves of an `$or` read\n * as a conjunction here, so compiling from this list would turn `a OR b` into\n * `a AND b`. Use {@link normalizeAnalyticsFilterTree} for that.\n */\nexport function collectFilterLeaves(\n node: NormalizedFilterNode | null,\n): NormalizedAnalyticsFilter[] {\n if (!node) return [];\n if (node.kind === 'leaf') return [{ member: node.member, operator: node.operator, values: node.values }];\n if (node.kind === 'not') return collectFilterLeaves(node.child);\n return node.children.flatMap(collectFilterLeaves);\n}\n\n/** Recover a finite number from a purely-numeric token, else undefined. */\nfunction recoverNumber(s: string): number | undefined {\n if (/^-?\\d+(\\.\\d+)?$/.test(s)) {\n const n = Number(s);\n if (Number.isFinite(n)) return n;\n }\n return undefined;\n}\n\n/**\n * Coerce a stringified filter value back into a runtime type for SQL\n * parameter binding. Better-sqlite3 (and most drivers) cannot bind a JS\n * boolean, so booleans are recovered as `1`/`0` integers; numbers are\n * recovered as numbers — avoiding string-vs-number mismatches against typed\n * columns.\n */\nexport function coerceFilterValueForSql(s: string): unknown {\n if (s === 'true') return 1;\n if (s === 'false') return 0;\n if (s === 'null') return null;\n return recoverNumber(s) ?? s;\n}\n\n/**\n * Coerce a stringified filter value back into a runtime type for the ObjectQL\n * aggregate engine. Unlike the SQL path, the engine compares against the\n * *stored* runtime type, so a boolean field holds a real `true`/`false` — bind\n * the boolean itself, NOT `1`/`0`, or the equality never matches.\n */\nexport function coerceFilterValueForObjectQL(s: string): unknown {\n if (s === 'true') return true;\n if (s === 'false') return false;\n if (s === 'null') return null;\n return recoverNumber(s) ?? s;\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { FilterCondition } from '@objectstack/spec/data';\n\n/**\n * Compile an RLS / tenant read-scope `FilterCondition` into a parameterized,\n * alias-qualified SQL predicate (ADR-0021 D-C).\n *\n * This is the single, security-critical translation point between the\n * canonical Mongo-style filter the `RLSCompiler` emits and the raw SQL the\n * analytics `NativeSQLStrategy` runs. It is deliberately:\n *\n * - **Fail-closed.** Any operator, value shape, or identifier it cannot\n * translate THROWS. A read-scope predicate must never be silently dropped —\n * dropping it would run the query unscoped and leak cross-tenant data.\n * - **Injection-safe.** Field/alias identifiers are validated against a strict\n * snake_case pattern and every value is bound as a `?` placeholder (the\n * strategy renumbers `?` → `$N`). No value is ever interpolated into SQL.\n * - **Alias-qualified.** Bare fields become `\"alias\".\"field\"` so the same\n * predicate applies to the base table or any joined table.\n *\n * Supports the operators the RLS layer and common policies emit: implicit\n * equality, `$eq/$ne/$gt/$gte/$lt/$lte/$in/$nin/$between/$contains/$notContains/\n * $startsWith/$endsWith/$null/$exists`, and `$and/$or/$not` combinators.\n */\n\nconst IDENT = /^[a-z_][a-z0-9_]*$/i;\n\nfunction quoteIdent(name: string, kind: string): string {\n if (typeof name !== 'string' || !IDENT.test(name)) {\n throw new Error(`[read-scope-sql] unsafe ${kind} identifier \"${String(name)}\" — refusing to build read scope (fail-closed).`);\n }\n return `\"${name}\"`;\n}\n\nexport function compileScopedFilterToSql(\n filter: FilterCondition,\n alias: string,\n): { sql: string; params: unknown[] } {\n const quotedAlias = quoteIdent(alias, 'alias');\n const params: unknown[] = [];\n const sql = compileNode(filter, quotedAlias, params);\n return { sql, params };\n}\n\n/** Compile a filter node into a boolean SQL expression ('' = empty/no constraint). */\nfunction compileNode(node: unknown, qAlias: string, params: unknown[]): string {\n if (node === null || typeof node !== 'object' || Array.isArray(node)) {\n throw new Error('[read-scope-sql] read scope must be a filter object (fail-closed).');\n }\n const clauses: string[] = [];\n for (const [key, value] of Object.entries(node as Record<string, unknown>)) {\n if (key === '$and' || key === '$or') {\n if (!Array.isArray(value) || value.length === 0) {\n throw new Error(`[read-scope-sql] \"${key}\" requires a non-empty array (fail-closed).`);\n }\n const parts = (value as unknown[])\n .map((child) => compileNode(child, qAlias, params))\n .filter((s) => s.length > 0);\n if (parts.length === 0) continue;\n const joiner = key === '$and' ? ' AND ' : ' OR ';\n clauses.push(`(${parts.join(joiner)})`);\n } else if (key === '$not') {\n const inner = compileNode(value, qAlias, params);\n if (inner) clauses.push(`NOT (${inner})`);\n } else if (key.startsWith('$')) {\n throw new Error(`[read-scope-sql] unsupported top-level operator \"${key}\" (fail-closed).`);\n } else {\n clauses.push(compileField(key, value, qAlias, params));\n }\n }\n return clauses.join(' AND ');\n}\n\n/** Compile a single `field: value | { $op: ... }` entry. */\nfunction compileField(field: string, value: unknown, qAlias: string, params: unknown[]): string {\n const col = `${qAlias}.${quoteIdent(field, 'field')}`;\n\n // Scalar / null → implicit equality.\n if (value === null) return `${col} IS NULL`;\n if (typeof value !== 'object' || value instanceof Date) {\n params.push(value);\n return `${col} = ?`;\n }\n if (Array.isArray(value)) {\n throw new Error(`[read-scope-sql] bare array value for \"${field}\" — use { $in: [...] } (fail-closed).`);\n }\n\n const ops = value as Record<string, unknown>;\n const keys = Object.keys(ops);\n // A value object must be ALL operators; a non-$ key means a nested relation,\n // which a flat read scope cannot join — fail closed.\n if (keys.length === 0 || keys.some((k) => !k.startsWith('$'))) {\n throw new Error(`[read-scope-sql] \"${field}\" has a nested/relation value which is not supported in a read scope (fail-closed).`);\n }\n\n const parts: string[] = [];\n for (const op of keys) {\n parts.push(compileOperator(col, op, ops[op], field, params));\n }\n return parts.length === 1 ? parts[0] : `(${parts.join(' AND ')})`;\n}\n\nfunction bind(params: unknown[], v: unknown): string {\n params.push(v);\n return '?';\n}\n\nfunction compileOperator(col: string, op: string, val: unknown, field: string, params: unknown[]): string {\n switch (op) {\n case '$eq': return val === null ? `${col} IS NULL` : `${col} = ${bind(params, val)}`;\n case '$ne': return val === null ? `${col} IS NOT NULL` : `${col} <> ${bind(params, val)}`;\n case '$gt': return `${col} > ${bind(params, val)}`;\n case '$gte': return `${col} >= ${bind(params, val)}`;\n case '$lt': return `${col} < ${bind(params, val)}`;\n case '$lte': return `${col} <= ${bind(params, val)}`;\n case '$in': {\n if (!Array.isArray(val)) throw new Error(`[read-scope-sql] $in for \"${field}\" needs an array (fail-closed).`);\n if (val.length === 0) return '1 = 0'; // IN () matches nothing — safe\n return `${col} IN (${val.map((v) => bind(params, v)).join(', ')})`;\n }\n case '$nin': {\n if (!Array.isArray(val)) throw new Error(`[read-scope-sql] $nin for \"${field}\" needs an array (fail-closed).`);\n if (val.length === 0) return '1 = 1'; // NOT IN () excludes nothing\n return `${col} NOT IN (${val.map((v) => bind(params, v)).join(', ')})`;\n }\n case '$between': {\n if (!Array.isArray(val) || val.length !== 2) throw new Error(`[read-scope-sql] $between for \"${field}\" needs [min,max] (fail-closed).`);\n return `${col} BETWEEN ${bind(params, val[0])} AND ${bind(params, val[1])}`;\n }\n case '$contains': return `${col} LIKE ${bind(params, `%${String(val)}%`)}`;\n case '$notContains': return `${col} NOT LIKE ${bind(params, `%${String(val)}%`)}`;\n case '$startsWith': return `${col} LIKE ${bind(params, `${String(val)}%`)}`;\n case '$endsWith': return `${col} LIKE ${bind(params, `%${String(val)}`)}`;\n case '$null': return val ? `${col} IS NULL` : `${col} IS NOT NULL`;\n case '$exists': return val ? `${col} IS NOT NULL` : `${col} IS NULL`;\n default:\n throw new Error(`[read-scope-sql] unsupported operator \"${op}\" on \"${field}\" (fail-closed).`);\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { AnalyticsQuery, AnalyticsResult } from '@objectstack/spec/contracts';\nimport type { Cube } from '@objectstack/spec/data';\nimport type { AnalyticsStrategy, StrategyContext } from './types.js';\nimport {\n normalizeAnalyticsFilterTree,\n coerceFilterValueForSql,\n type NormalizedFilterNode,\n} from './filter-normalizer.js';\nimport { compileScopedFilterToSql } from '../read-scope-sql.js';\nimport { nextUtcCalendarDay } from '@objectstack/core';\n\n/**\n * The SQL wrapper for each aggregate a measure's `type` can name.\n *\n * A table rather than a `switch` so its coverage is *assertable*: the aggregate\n * vocabulary lives in `@objectstack/spec` (`AggregationFunction`), the dataset\n * compiler subtracts the two it cannot lower (`array_agg`, `string_agg`), and\n * `aggregation-lockstep.test.ts` checks that what remains is exactly the keys\n * below. A `switch` gave that no purchase — the missing case fell to\n * `default: COUNT(*)`, so an aggregate the spec grew would have returned a row\n * count instead of the number the author asked for, silently. objectui#2945.\n *\n * Non-aggregate metric types (`number`/`string`/`boolean`) are deliberately\n * absent — they are handled by {@link EXPRESSION_METRIC_TYPES}, which emits the\n * author's expression rather than wrapping it.\n */\nconst AGGREGATE_SQL: Record<string, (col: string) => string> = {\n 'count': () => 'COUNT(*)',\n 'sum': (col) => `SUM(${col})`,\n 'avg': (col) => `AVG(${col})`,\n 'min': (col) => `MIN(${col})`,\n 'max': (col) => `MAX(${col})`,\n 'count_distinct': (col) => `COUNT(DISTINCT ${col})`,\n};\n\n/** Exported for the lockstep guard — the aggregates this strategy can lower. */\nexport const SUPPORTED_AGGREGATE_SQL_KEYS = Object.keys(AGGREGATE_SQL);\n\n/**\n * Metric types that are a custom SQL *expression*, not an aggregate to wrap.\n *\n * `AggregationMetricType` (`data/analytics.zod.ts`) documents these three as\n * \"Custom SQL expression returning a number / string / boolean\" — the measure's\n * `sql` IS the whole computation (a ratio, a `CASE`, a window function), so the\n * only correct emission is the expression itself. They used to fall through to\n * `resolveMeasureSql`'s `COUNT(*)` fallback, which threw the expression away and\n * returned a row count. #4157.\n *\n * Named rather than derived as \"everything that is not an aggregate\": deriving it\n * would silently classify a *new* aggregate the spec grows (`median`, …) as an\n * expression and emit a bare column. `metric-type-coverage.test.ts` asserts these\n * two sets partition `AggregationMetricType`, so a new member fails a test\n * instead of picking a default.\n */\nexport const EXPRESSION_METRIC_TYPES = new Set(['number', 'string', 'boolean']);\n\n/**\n * A dot-separated chain of bare identifiers — `amount`, `account.amount`,\n * `account.owner.region`. Distinguishes a relationship PATH, which\n * {@link NativeSQLStrategy.qualifyAndRegisterJoin} lowers into joins, from a SQL\n * expression that merely contains a dot. #4157.\n */\nconst IDENTIFIER_PATH = /^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)*$/;\n\n/**\n * NativeSQLStrategy — Priority 1\n *\n * Pushes the analytics query down to the database as a native SQL statement.\n * This is the most efficient path and is preferred whenever the backing driver\n * supports raw SQL execution (e.g. Postgres, MySQL, SQLite).\n *\n * `resolveMeasureSql` used to answer `COUNT(*)` to three different questions it\n * could not otherwise answer — an undeclared measure, a custom-SQL-expression\n * metric type, and an unrecognised type. All three returned a plausible number\n * for a query that asked for something else. They now emit the expression or\n * throw; see that method. #4157.\n */\nexport class NativeSQLStrategy implements AnalyticsStrategy {\n readonly name = 'NativeSQLStrategy';\n readonly priority = 10;\n\n canHandle(query: AnalyticsQuery, ctx: StrategyContext): boolean {\n if (!query.cube) return false;\n // This strategy groups by the raw column expression (`GROUP BY <col>`) and\n // emits no `date_trunc` — it cannot bucket a date dimension to a coarser\n // granularity, nor resolve buckets on a non-UTC calendar. When the query\n // asks for granularity bucketing we therefore DECLINE so the lower-priority\n // ObjectQLStrategy handles it via `engine.aggregate` (native date_trunc when\n // UTC-safe, else uniform in-memory bucketing). Without this, a date-bucketed\n // query silently grouped by the raw timestamp — one bucket per row — and a\n // non-UTC reference timezone was ignored entirely (ADR-0053 Phase 2, #1982).\n if (query.timeDimensions?.some((td) => !!td.granularity)) return false;\n // ADR-0062 D6 — DECLINE federated (external-datasource) objects. This\n // strategy hand-compiles `FROM \"<object>\"` and bare column references, which\n // bypass the driver's physical-table resolution (`external.remoteName` /\n // `remoteSchema` / `columnMap`) and would query the WRONG table. Routing the\n // query to the lower-priority ObjectQL aggregate path keeps it correct —\n // that path goes through the driver's `getBuilder` (#2138/#2149). Applies to\n // the base object AND any joined object (a join would also hit the wrong\n // table). Until native-SQL learns the driver's resolution, \"disabled\" beats\n // \"silently wrong\".\n if (typeof ctx.isExternalObject === 'function') {\n const cube = ctx.getCube(query.cube);\n if (cube) {\n if (ctx.isExternalObject(this.extractObjectName(cube))) return false;\n const joinTargets = cube.joins ? Object.values(cube.joins) : [];\n for (const j of joinTargets) {\n const joinedObject = (j as { name?: string })?.name;\n if (joinedObject && ctx.isExternalObject(joinedObject)) return false;\n }\n }\n }\n const caps = ctx.queryCapabilities(query.cube);\n return caps.nativeSql && typeof ctx.executeRawSql === 'function';\n }\n\n async execute(query: AnalyticsQuery, ctx: StrategyContext): Promise<AnalyticsResult> {\n const { sql, params } = await this.generateSql(query, ctx);\n const cube = ctx.getCube(query.cube!)!;\n const objectName = this.extractObjectName(cube);\n\n const rows = await ctx.executeRawSql!(objectName, sql, params);\n\n // Build field metadata\n const fields = this.buildFieldMeta(query, cube);\n\n return { rows, fields, sql };\n }\n\n async generateSql(query: AnalyticsQuery, ctx: StrategyContext): Promise<{ sql: string; params: unknown[] }> {\n const cube = ctx.getCube(query.cube!);\n if (!cube) {\n throw new Error(`Cube not found: ${query.cube}`);\n }\n\n const params: unknown[] = [];\n const selectClauses: string[] = [];\n const groupByClauses: string[] = [];\n const tableName = this.extractObjectName(cube);\n // Map of relation alias → JOIN clause. Populated lazily as dotted\n // dimensions/measures/filters are resolved.\n const joins = new Map<string, string>();\n\n // Build SELECT for dimensions\n if (query.dimensions && query.dimensions.length > 0) {\n for (const dim of query.dimensions) {\n const colExpr = this.resolveDimensionSql(cube, dim, tableName, joins);\n selectClauses.push(`${colExpr} AS \"${dim}\"`);\n groupByClauses.push(colExpr);\n }\n }\n\n // Build SELECT for measures\n if (query.measures && query.measures.length > 0) {\n for (const measure of query.measures) {\n const aggExpr = this.resolveMeasureSql(cube, measure, tableName, joins);\n selectClauses.push(`${aggExpr} AS \"${measure}\"`);\n }\n }\n\n // Build WHERE clause. The filter is a TREE, so it compiles recursively —\n // a flat loop can only ever AND, which is precisely why an author's `$or`\n // used to be dropped instead of compiled.\n const whereClauses: string[] = [];\n const filterSql = this.compileFilterNode(\n normalizeAnalyticsFilterTree(query),\n cube,\n tableName,\n joins,\n params,\n ctx,\n );\n if (filterSql) whereClauses.push(filterSql);\n\n // Build time dimension filters\n if (query.timeDimensions && query.timeDimensions.length > 0) {\n for (const td of query.timeDimensions) {\n const colExpr = this.resolveFieldSql(cube, td.dimension, tableName, joins);\n if (td.dateRange) {\n const range = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];\n if (range.length === 2) {\n // Same epoch-vs-text root cause as buildFilterClause: a dateRange on a\n // SQLite `Field.datetime` column compares ISO TEXT against an INTEGER\n // epoch and matches nothing. Coerce both bounds to the storage form —\n // and normalise the column to that form too, because the column holds\n // BOTH forms at once and coercing only the bounds still empties the\n // half the writer stored the other way (#3912).\n const td2 = this.resolveStorageTarget(cube, td.dimension, tableName);\n const column = this.temporalColumn(ctx, td2, colExpr);\n // A bare-day window end means \"through that whole day\" (#3777). A\n // BETWEEN's inclusive upper bound anchors a bare `YYYY-MM-DD` to\n // midnight on a datetime column, dropping the final day's rows, so\n // the window compiles half-open — `>= start AND < end+1day` — the\n // same `[gte, lt)` the drill ranges emit. Equivalent to the old\n // BETWEEN for a `date` column (plain `YYYY-MM-DD` ordering), which\n // is what lets this path stay column-type-blind.\n const nextDay = nextUtcCalendarDay(range[1]);\n params.push(this.coerceTemporal(ctx, td2, range[0]));\n const lower = `${column} >= $${params.length}`;\n if (nextDay != null) {\n params.push(this.coerceTemporal(ctx, td2, nextDay));\n whereClauses.push(`(${lower} AND ${column} < $${params.length})`);\n } else {\n params.push(this.coerceTemporal(ctx, td2, range[1]));\n whereClauses.push(`(${lower} AND ${column} <= $${params.length})`);\n }\n }\n }\n }\n }\n\n // ── ADR-0021 D-C — enforce the join allowlist + inject per-object RLS ──\n // 1. Reject any join not backed by a relationship the dataset declared.\n const allowed = ctx.getAllowedRelationships?.(query.cube!);\n if (allowed) {\n for (const alias of joins.keys()) {\n if (!allowed.has(alias)) {\n throw new Error(\n `[NativeSQLStrategy] join \"${alias}\" is not backed by a declared relationship on ` +\n `cube \"${query.cube}\". v1 only joins along relationships listed in the dataset's \\`include\\`.`,\n );\n }\n }\n }\n // 2. Inject the tenant/RLS read scope for the base table AND every joined\n // object — this is the predicate the raw-SQL path would otherwise skip.\n this.applyReadScope(this.extractObjectName(cube), tableName, ctx, whereClauses, params);\n for (const alias of joins.keys()) {\n // The joined OBJECT (for the RLS lookup) is the target table from the\n // cube's join map; the ALIAS is how it's referenced in SQL. These differ\n // for namespaced objects (alias `account` → object `crm_account`).\n const joinedObject = cube.joins?.[alias]?.name ?? alias;\n this.applyReadScope(joinedObject, alias, ctx, whereClauses, params);\n }\n\n let sql = `SELECT ${selectClauses.join(', ')} FROM \"${tableName}\"`;\n if (joins.size > 0) {\n sql += ' ' + Array.from(joins.values()).join(' ');\n }\n if (whereClauses.length > 0) {\n sql += ` WHERE ${whereClauses.join(' AND ')}`;\n }\n if (groupByClauses.length > 0) {\n sql += ` GROUP BY ${groupByClauses.join(', ')}`;\n }\n if (query.order && Object.keys(query.order).length > 0) {\n const orderClauses = Object.entries(query.order).map(([f, d]) => `\"${f}\" ${d.toUpperCase()}`);\n sql += ` ORDER BY ${orderClauses.join(', ')}`;\n }\n if (query.limit != null) {\n sql += ` LIMIT ${query.limit}`;\n }\n if (query.offset != null) {\n sql += ` OFFSET ${query.offset}`;\n }\n\n return { sql, params };\n }\n\n // ── Helpers ──────────────────────────────────────────────────────\n\n /**\n * ADR-0021 D-C — inject an object's read scope (tenant + RLS predicate) into\n * the WHERE clause. The scope is a canonical `FilterCondition` (what the\n * RLSCompiler emits); `compileScopedFilterToSql` turns it into alias-qualified,\n * parameterized SQL (fail-closed — it throws rather than drop a predicate).\n * The `?` placeholders are then renumbered into the strategy's `$N` scheme.\n * No-op when the runtime provides no scope hook (the caller is then\n * responsible for isolation — see contract note).\n */\n private applyReadScope(\n objectName: string,\n alias: string,\n ctx: StrategyContext,\n whereClauses: string[],\n params: unknown[],\n ): void {\n if (typeof ctx.getReadScope !== 'function') return;\n const filter = ctx.getReadScope(objectName);\n if (filter === undefined || filter === null) return;\n const { sql, params: scopeParams } = compileScopedFilterToSql(filter, alias);\n if (!sql) return;\n let i = 0;\n const rendered = sql.replace(/\\?/g, () => {\n params.push(scopeParams[i++]);\n return `$${params.length}`;\n });\n whereClauses.push(`(${rendered})`);\n }\n\n /** SQL-safe join alias for a relationship path (dots → `__`); single-segment\n * paths are unchanged. Mirrors the dataset compiler's `cube.joins` keying so\n * alias, allowlist, and per-hop RLS all agree on one valid identifier. */\n private joinAlias(path: string): string {\n return path.replace(/\\./g, '__');\n }\n\n /**\n * Resolve a dimension/measure/filter SQL expression that may reference a\n * related table via dot notation (e.g. `account.industry`).\n *\n * A dotted `sql` is a relationship PATH (ADR-0071 multi-hop): every segment\n * but the last is a to-one relationship hop, the last is the column. Each hop\n * synthesises a `LEFT JOIN` aliased by its full path prefix, chained\n * parent→child. The convention (matching the auto-cube generator and\n * ObjectStack object schemas) for a single hop is:\n *\n * <parentTable>.<lookupField> = <lookupField>.id\n *\n * i.e. the lookup field name on the parent table equals the related\n * table name. This holds for all `Field.lookup({ object: '...' })`\n * declarations where the field is named after its target object.\n *\n * Returns the qualified SQL reference (e.g. `\"account\".\"industry\"`).\n * Pure column references (no dot) are returned as-is.\n */\n private qualifyAndRegisterJoin(\n rawSql: string,\n parentTable: string,\n joins: Map<string, string>,\n cube?: Cube,\n ): string {\n if (!rawSql.includes('.')) {\n // Base-table column. When the cube can join other tables, a bare column\n // that also exists on a joined table (e.g. base `status` vs joined\n // `account.status`) makes the SQL engine raise \"ambiguous column name\".\n // Qualify plain identifiers with the base table; leave SQL expressions\n // and `*` untouched. Single-object cubes (no joins) keep bare columns so\n // their generated SQL is byte-for-byte unchanged.\n const canJoin = !!cube?.joins && Object.keys(cube.joins).length > 0;\n if (canJoin && /^[A-Za-z_][A-Za-z0-9_]*$/.test(rawSql)) {\n return `\"${parentTable}\".\"${rawSql}\"`;\n }\n return rawSql;\n }\n // A dot does not by itself mean \"relationship path\". `SUM(account.amount)`\n // is one SQL EXPRESSION that happens to contain a dot, and splitting it as a\n // path produced `\"SUM(account\".\"amount)\"` plus a phantom\n // `LEFT JOIN \"SUM(account\"` — invalid SQL and a join to a table that does not\n // exist. Only qualify when every segment is a bare identifier; otherwise the\n // author wrote an expression and it is returned as-is. #4157.\n if (!IDENTIFIER_PATH.test(rawSql)) return rawSql;\n // Multi-hop (ADR-0071): the dotted path IS the join chain. Every segment but\n // the last is a relationship hop; the last is the column. The join ALIAS at\n // each hop is the full path PREFIX (`account`, then `account.owner`), which\n // encodes its own parent (the prefix minus its last segment) and FK column\n // (that segment). Register one LEFT JOIN per prefix, chaining parent→child.\n const segments = rawSql.split('.');\n const column = segments[segments.length - 1];\n const hops = segments.slice(0, -1);\n if (hops.length === 0 || !column) return rawSql;\n let parentAlias = parentTable;\n let prefix = '';\n for (const seg of hops) {\n prefix = prefix ? `${prefix}.${seg}` : seg;\n const alias = this.joinAlias(prefix);\n if (!joins.has(alias)) {\n // The joined TABLE is resolved from the Cube's `joins` map (emitted by\n // the dataset compiler, keyed by the same alias); fall back to the alias\n // as the table for legacy/same-name cubes.\n const joinTable = cube?.joins?.[alias]?.name ?? alias;\n // Only emit an explicit alias when the table differs from it; when they\n // match, `LEFT JOIN \"account\" ON …` is cleaner (and back-compat).\n const tableRef = joinTable === alias ? `\"${alias}\"` : `\"${joinTable}\" \"${alias}\"`;\n joins.set(\n alias,\n `LEFT JOIN ${tableRef} ON \"${parentAlias}\".\"${seg}\" = \"${alias}\".\"id\"`,\n );\n }\n parentAlias = alias;\n }\n return `\"${parentAlias}\".\"${column}\"`;\n }\n\n /**\n * Resolve a member reference (dimension, measure, or filter field) to its\n * cube definition.\n *\n * Accepts three naming conventions:\n * 1. `<cube>.<field>` — the canonical analytics qualifier (stripped to `<field>`).\n * 2. `<lookup>.<field>` — a relation traversal (e.g. `account.industry`).\n * First tried as the literal key, then as the underscore-flattened\n * key (`account_industry`), and finally returned as a synthetic\n * definition whose `sql` is the dotted reference so the JOIN\n * machinery can pick it up.\n * 3. `<field>` — a bare field name on the cube's table.\n */\n private lookupMember(\n cube: Cube,\n member: string,\n kind: 'dimension' | 'measure',\n ): { sql: string; type?: string } | undefined {\n const bag = kind === 'dimension' ? cube.dimensions : cube.measures;\n // Direct hit on the registered key (handles `cube.field` and exact dotted keys).\n if (bag[member]) return bag[member];\n if (member.includes('.')) {\n const [first, ...rest] = member.split('.');\n const tail = rest.join('.');\n // `<cube>.<field>` style.\n if (first === cube.name && bag[tail]) return bag[tail];\n // Plain second-segment lookup (legacy behaviour).\n if (bag[tail]) return bag[tail];\n // Underscore-flattened relation lookup (e.g. `account_industry`).\n const flat = member.replace(/\\./g, '_');\n if (bag[flat]) return bag[flat];\n // Synthetic relation traversal — let qualifyAndRegisterJoin handle it.\n if (kind === 'dimension') {\n return { sql: member, type: 'string' };\n }\n } else if (bag[member]) {\n return bag[member];\n }\n return undefined;\n }\n\n private resolveDimensionSql(\n cube: Cube,\n member: string,\n parentTable: string,\n joins: Map<string, string>,\n ): string {\n const dim = this.lookupMember(cube, member, 'dimension');\n const raw = dim ? dim.sql : (member.includes('.') ? member.split('.')[1] : member);\n return this.qualifyAndRegisterJoin(raw, parentTable, joins, cube);\n }\n\n private resolveMeasureSql(\n cube: Cube,\n member: string,\n parentTable: string,\n joins: Map<string, string>,\n ): string {\n const measure = this.lookupMember(cube, member, 'measure') as\n | { sql: string; type: string }\n | undefined;\n // `lookupMember`'s synthetic relation fallback is dimension-only, so an\n // undeclared measure name lands here — a typo, or a query naming a metric\n // this cube does not have. It used to return `COUNT(*)`: the caller asked\n // for revenue and got a row count, aliased AS \"revenue\". #4157.\n if (!measure) {\n const declared = Object.keys(cube.measures ?? {});\n throw new Error(\n `[native-sql-strategy] cube \"${cube.name}\" declares no measure \"${member}\"` +\n (declared.length ? ` (declared: ${declared.join(', ')})` : ' (it declares none)'),\n );\n }\n\n const col = measure.sql === '*'\n ? '*'\n : this.qualifyAndRegisterJoin(measure.sql, parentTable, joins, cube);\n\n const wrap = AGGREGATE_SQL[measure.type];\n if (wrap) return wrap(col);\n // A custom SQL expression: the measure's `sql` IS the computation, so emit\n // it unwrapped. In a grouped query the expression must itself be\n // aggregate-shaped — measures never join `GROUP BY` (only dimensions do), so\n // a scalar expression there is invalid SQL. That is the author's contract to\n // keep; silently substituting `COUNT(*)` did not keep it for them.\n if (EXPRESSION_METRIC_TYPES.has(measure.type)) return col;\n\n throw new Error(\n `[native-sql-strategy] measure \"${member}\" on cube \"${cube.name}\" has ` +\n `unrecognised type \"${measure.type}\" — expected an aggregate ` +\n `(${SUPPORTED_AGGREGATE_SQL_KEYS.join(', ')}) or a custom-expression type ` +\n `(${[...EXPRESSION_METRIC_TYPES].join(', ')}).`,\n );\n }\n\n private resolveFieldSql(\n cube: Cube,\n member: string,\n parentTable: string,\n joins: Map<string, string>,\n ): string {\n const dim = this.lookupMember(cube, member, 'dimension');\n if (dim) return this.qualifyAndRegisterJoin(dim.sql, parentTable, joins, cube);\n const measure = this.lookupMember(cube, member, 'measure');\n if (measure) return this.qualifyAndRegisterJoin(measure.sql, parentTable, joins, cube);\n const fieldName = member.includes('.') ? member.split('.')[1] : member;\n return fieldName;\n }\n\n /**\n * Resolve the (object, column) a filter member binds against, so its\n * comparand can be coerced to that column's on-disk storage form.\n *\n * Mirrors `resolveFieldSql`'s `sql` resolution but yields the *logical*\n * target rather than the qualified SQL:\n * - A dotted column (`account.region`, emitted for a relation traversal)\n * belongs to the JOINED object — resolve the alias → target table via the\n * cube's `joins` map (alias `account` → object `crm_account` when\n * namespaced) and take the tail as the column.\n * - Otherwise the column lives on the cube's BASE table. Use the dimension's\n * resolved `sql` (the real column, which may differ from the member name,\n * e.g. dimension `assessed` → column `assessed_at`) rather than the member.\n */\n private resolveStorageTarget(\n cube: Cube,\n member: string,\n baseTable: string,\n ): { object: string; field: string } {\n const dim = this.lookupMember(cube, member, 'dimension');\n const measure = dim ? undefined : this.lookupMember(cube, member, 'measure');\n const rawSql = dim?.sql ?? measure?.sql ?? (member.includes('.') ? member.split('.').slice(1).join('.') : member);\n\n if (rawSql.includes('.')) {\n // Multi-hop (ADR-0071): the column's owning object is the join at the\n // relationship PATH (all segments but the last); the column is the last.\n const segments = rawSql.split('.');\n const field = segments[segments.length - 1];\n const relPath = segments.slice(0, -1).join('.');\n const object = cube.joins?.[this.joinAlias(relPath)]?.name ?? relPath;\n return { object, field };\n }\n return { object: baseTable, field: rawSql };\n }\n\n /**\n * Apply the storage-form coercion for a single comparand. Prefers the\n * driver-backed `coerceTemporalFilterValue` hook (single source of truth for\n * the date/datetime storage convention — see StrategyContext); when the hook\n * is absent, or returns the value unchanged (the field is not a temporal\n * column, or the dialect stores it as a native timestamp), falls back to the\n * generic boolean/number recovery so non-temporal typed columns still bind\n * correctly.\n */\n private coerceTemporal(\n ctx: StrategyContext,\n target: { object: string; field: string },\n value: string,\n ): unknown {\n if (typeof ctx.coerceTemporalFilterValue === 'function') {\n const coerced = ctx.coerceTemporalFilterValue(target.object, target.field, value);\n // Hook returns the value untouched for non-temporal / native-timestamp\n // columns; only short-circuit when it actually changed the value.\n if (coerced !== value) return coerced;\n }\n return coerceFilterValueForSql(value);\n }\n\n /**\n * The column side of {@link coerceTemporal}: normalise the reference so it\n * reads in the storage form the comparand was coerced into.\n *\n * A SQLite `Field.datetime` column carries an INTEGER epoch (a `Date` write)\n * and ISO TEXT (a REST/JSON write, a `NOW()` default — including the platform's\n * own `created_at`) at the SAME time, so coercing the value alone fixes one half\n * and empties the other. That is #3912: a `dateRange: last_30_days` on\n * `created_date` read 0 with 29 rows in range. Every other column and dialect\n * gets its reference back verbatim.\n */\n private temporalColumn(\n ctx: StrategyContext,\n target: { object: string; field: string },\n col: string,\n ): string {\n if (typeof ctx.coerceTemporalFilterColumn !== 'function') return col;\n return ctx.coerceTemporalFilterColumn(target.object, target.field, col) || col;\n }\n\n /**\n * Compile a normalized filter node into a boolean SQL expression, recursing\n * through the combinators. `null` = no constraint.\n *\n * Leaves go through {@link buildFilterClause} exactly as they did when this\n * was a flat loop, so the storage-form coercion and the calendar-day\n * upper-bound rule (#3777) apply at every depth — including inside an `$or`,\n * where a second, combinator-aware implementation would have been free to\n * drift from the first.\n *\n * Parenthesisation is explicit rather than left to SQL's precedence: `AND`\n * does bind tighter than `OR`, so `a AND b OR c` happens to be right, but\n * being right by construction is what keeps a future edit from making it\n * wrong.\n */\n private compileFilterNode(\n node: NormalizedFilterNode | null,\n cube: Cube,\n parentTable: string,\n joins: Map<string, string>,\n params: unknown[],\n ctx: StrategyContext,\n ): string | null {\n if (!node) return null;\n\n if (node.kind === 'leaf') {\n const colExpr = this.resolveFieldSql(cube, node.member, parentTable, joins);\n // Resolve the (object, column) this member binds against so the value\n // can be coerced to the column's storage form (see buildFilterClause).\n const target = this.resolveStorageTarget(cube, node.member, parentTable);\n return this.buildFilterClause(colExpr, node.operator, node.values, params, ctx, target);\n }\n\n if (node.kind === 'not') {\n const inner = this.compileFilterNode(node.child, cube, parentTable, joins, params, ctx);\n return inner ? `NOT (${inner})` : null;\n }\n\n const parts = node.children\n .map((child) => this.compileFilterNode(child, cube, parentTable, joins, params, ctx))\n .filter((s): s is string => !!s);\n if (parts.length === 0) return null;\n if (parts.length === 1) return parts[0];\n return `(${parts.join(node.kind === 'or' ? ' OR ' : ' AND ')})`;\n }\n\n private buildFilterClause(\n rawCol: string,\n operator: string,\n values: string[] | undefined,\n params: unknown[],\n ctx: StrategyContext,\n target: { object: string; field: string },\n ): string | null {\n const opMap: Record<string, string> = {\n equals: '=', notEquals: '!=', gt: '>', gte: '>=', lt: '<', lte: '<=',\n contains: 'LIKE', notContains: 'NOT LIKE',\n startsWith: 'LIKE', endsWith: 'LIKE',\n };\n /** The LIKE pattern each string operator wraps its comparand in. */\n const likePattern: Record<string, (v: string) => string> = {\n contains: (v) => `%${v}%`,\n notContains: (v) => `%${v}%`,\n startsWith: (v) => `${v}%`,\n endsWith: (v) => `%${v}`,\n };\n\n // Null predicates and the LIKE family read the column as stored — the former\n // is storage-independent, the latter is a substring match on the raw text —\n // so only the value comparisons take the normalised reference.\n if (operator === 'set') return `${rawCol} IS NOT NULL`;\n if (operator === 'notSet') return `${rawCol} IS NULL`;\n\n if (operator === 'in' || operator === 'notIn') {\n if (!values || values.length === 0) return null;\n // Dates can legitimately appear in an `in`/`notIn` set (e.g. a multi-day\n // KPI), so coerce each element to the column's storage form too — same\n // SQLite epoch-vs-text root cause as the scalar operators below.\n const placeholders = values.map(v => { params.push(this.coerceTemporal(ctx, target, v)); return `$${params.length}`; }).join(', ');\n return `${this.temporalColumn(ctx, target, rawCol)} ${operator === 'in' ? 'IN' : 'NOT IN'} (${placeholders})`;\n }\n\n const sqlOp = opMap[operator];\n if (!sqlOp || !values || values.length === 0) return null;\n\n // The LIKE family reads the column as stored — a substring/prefix/suffix\n // match is on the raw text — so it keeps the un-normalised reference.\n const pattern = likePattern[operator];\n if (pattern) {\n params.push(pattern(values[0]));\n return `${rawCol} ${sqlOp} $${params.length}`;\n }\n\n // A bare-day `lte` bound means \"through that whole day\" (#3777): compile\n // half-open (`< day+1`) so a datetime column keeps the final day's rows.\n // Equivalent to `<=` for a `date` column, so no column-type lookup needed.\n if (operator === 'lte') {\n const nextDay = nextUtcCalendarDay(values[0]);\n if (nextDay != null) {\n params.push(this.coerceTemporal(ctx, target, nextDay));\n return `${this.temporalColumn(ctx, target, rawCol)} < $${params.length}`;\n }\n }\n\n // Coerce so booleans/numbers bind as their native SQL types AND so a\n // relative-date / ISO-string comparand on a SQLite `Field.datetime`\n // column is converted to its INTEGER epoch storage form. Without this a\n // dashboard filter like `assessed_at >= '2025-06-18'` compiles to a\n // TEXT-vs-INTEGER affinity compare that is always false → \"No rows\",\n // even though the rows exist (the confirmed time-series chart bug).\n params.push(this.coerceTemporal(ctx, target, values[0]));\n return `${this.temporalColumn(ctx, target, rawCol)} ${sqlOp} $${params.length}`;\n }\n\n private extractObjectName(cube: Cube): string {\n return cube.sql.trim();\n }\n\n private buildFieldMeta(query: AnalyticsQuery, cube: Cube): Array<{ name: string; type: string }> {\n const fields: Array<{ name: string; type: string }> = [];\n if (query.dimensions) {\n for (const dim of query.dimensions) {\n const d = this.lookupMember(cube, dim, 'dimension');\n fields.push({ name: dim, type: d?.type || 'string' });\n }\n }\n if (query.measures) {\n for (const m of query.measures) {\n fields.push({ name: m, type: 'number' });\n }\n }\n return fields;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { AnalyticsQuery, AnalyticsResult } from '@objectstack/spec/contracts';\nimport type { Cube } from '@objectstack/spec/data';\nimport type { AnalyticsStrategy, StrategyContext } from './types.js';\nimport {\n normalizeAnalyticsFilterTree,\n collectFilterLeaves,\n coerceFilterValueForObjectQL,\n type NormalizedFilterNode,\n} from './filter-normalizer.js';\nimport { compileScopedFilterToSql } from '../read-scope-sql.js';\nimport { nextUtcCalendarDay } from '@objectstack/core';\nimport {\n rebucketCrossObject,\n RECOMBINABLE_METHODS,\n type CrossObjectDim,\n type MeasureRecombine,\n type RecombinableMethod,\n} from './cross-object-rebucket.js';\n\n/** Scalar analytics operators → their SQL spelling (display SQL only). */\nconst SCALAR_SQL_OPS: Record<string, string> = {\n equals: '=', notEquals: '!=', gt: '>', gte: '>=', lt: '<', lte: '<=',\n};\n\n/** One cross-object grouping dimension planned for FK-expand (#3654). */\ninterface CrossObjectPlanDim {\n /** The caller's dimension name (output key), e.g. `region`. */\n outputName: string;\n /** The base lookup FK column to group the base aggregate by, e.g. `account`. */\n fkField: string;\n /** The related object's attribute to resolve the FK to, e.g. `region`. */\n attr: string;\n /** The related object name (join target), e.g. `crm_account`. */\n refObject: string;\n}\n\ninterface CrossObjectPlan {\n crossDims: CrossObjectPlanDim[];\n}\n\n/**\n * ObjectQLStrategy — Priority 2\n *\n * Translates an analytics query into an ObjectQL `engine.aggregate()` call.\n * This path works with any driver that supports the ObjectQL aggregate AST\n * (Postgres, Mongo, SQLite, etc.) without requiring raw SQL access.\n */\nexport class ObjectQLStrategy implements AnalyticsStrategy {\n readonly name = 'ObjectQLStrategy';\n readonly priority = 20;\n\n canHandle(query: AnalyticsQuery, ctx: StrategyContext): boolean {\n if (!query.cube) return false;\n const caps = ctx.queryCapabilities(query.cube);\n return caps.objectqlAggregate && typeof ctx.executeAggregate === 'function';\n }\n\n async execute(query: AnalyticsQuery, ctx: StrategyContext): Promise<AnalyticsResult> {\n const cube = ctx.getCube(query.cube!)!;\n const objectName = this.extractObjectName(cube);\n\n // Build groupBy from dimensions, honouring `timeDimensions` granularity.\n // A date dimension with a granularity becomes a STRUCTURED groupBy item\n // `{ field, dateGranularity }` — which `engine.aggregate()` buckets (driver\n // date_trunc or in-memory). Without this the ObjectQL path grouped raw\n // timestamps (one bucket per row) and date-bucketed dataset widgets never\n // matched their legacy `categoryGranularity` counterpart.\n type GroupByItem = string | { field: string; dateGranularity: string };\n const granByDim = new Map<string, string>();\n for (const td of query.timeDimensions ?? []) {\n if (td.granularity) granByDim.set(td.dimension, td.granularity);\n }\n const groupBy: GroupByItem[] = [];\n if (query.dimensions && query.dimensions.length > 0) {\n for (const dim of query.dimensions) {\n const field = this.resolveFieldName(cube, dim, 'dimension');\n const gran = granByDim.get(dim);\n groupBy.push(gran ? { field, dateGranularity: gran } : field);\n granByDim.delete(dim);\n }\n }\n // Time dimensions not also listed in `dimensions` still bucket + group.\n for (const [dim, gran] of granByDim) {\n groupBy.push({ field: this.resolveFieldName(cube, dim, 'dimension'), dateGranularity: gran });\n }\n\n // Build aggregations from measures\n const aggregations: Array<{ field: string; method: string; alias: string }> = [];\n if (query.measures && query.measures.length > 0) {\n for (const measure of query.measures) {\n const { field, method } = this.resolveMeasureAggregation(cube, measure);\n aggregations.push({ field, method, alias: measure });\n }\n }\n\n // Build the engine filter. Every predicate — the caller's `where` and the\n // time-dimension windows alike — is contributed through\n // `mergeFilterOperand`, because one field routinely carries MULTIPLE\n // operators (a range `{$gte, $lte}` on `close_date`) and a plain assignment\n // would keep only the last.\n const filter: Record<string, unknown> = {};\n // Operands that cannot merge into their field's entry without one silently\n // replacing the other; ANDed in below so the engine intersects them.\n const conjuncts: Record<string, unknown>[] = [];\n this.applyFilterNode(normalizeAnalyticsFilterTree(query), cube, filter, conjuncts);\n // #3650 — and the time-dimension WINDOWS, through the SAME merge, so a\n // `dateRange` and a caller `where` bound on one field compose instead of\n // clobbering each other.\n for (const { field, bounds } of this.dateRangeBounds(cube, query)) {\n const extra = this.mergeFilterOperand(filter, field, bounds);\n if (extra) conjuncts.push(extra);\n }\n if (conjuncts.length > 0) {\n filter.$and = [...(Array.isArray(filter.$and) ? filter.$and : []), ...conjuncts];\n }\n\n // #3654 — classify cross-object references. A cross-object DIMENSION within\n // the supported envelope is served by an FK-expand (`executeCrossObject`);\n // everything the engine cannot serve (cross-object measures/filters,\n // multi-hop, non-recombinable measures) is REJECTED by `planCrossObject` —\n // the engine has no join, and a silent mis-bucket is worse than a loud\n // error. `null` ⇒ the query is base-only and takes the direct path below.\n const plan = this.planCrossObject(cube, query, filter);\n if (plan) {\n return this.executeCrossObject(cube, query, aggregations, filter, plan, ctx);\n }\n\n // ADR-0021 D-C — the base object's read scope (tenant + RLS) MUST be ANDed\n // in before the query leaves the strategy (#3597). A base-only query has a\n // single object in play, so one base-object scope is sufficient here.\n const rows = await ctx.executeAggregate!(objectName, {\n // Structured groupBy items ({field, dateGranularity}) pass through the\n // executeAggregate bridge to engine.aggregate, which buckets them. The\n // contract types groupBy as string[]; the cast carries the richer shape.\n groupBy: groupBy.length > 0 ? (groupBy as unknown as string[]) : undefined,\n aggregations: aggregations.length > 0 ? aggregations : undefined,\n filter: this.withReadScope(objectName, filter, ctx),\n // ADR-0053 Phase 2 (D2): forward the reference tz so date buckets resolve\n // on that zone's calendar days. A non-UTC zone makes the engine bucket\n // in-memory (uniform across drivers); UTC/unset keeps the DB fast path.\n timezone: query.timezone,\n // ADR-0021 D-C (#3602): the second belt. `withReadScope` above is this\n // layer's own scoping; handing the engine the context makes ITS middleware\n // inject RLS too, so a future strategy that forgets `withReadScope` still\n // cannot read across tenants. Without it the operation reaches the engine\n // principal-less and plugin-security falls open — the #3597 shape.\n context: ctx.context,\n });\n\n // Remap short field names back to cube-qualified names. Driven by\n // `projectedDimensions`, so a `timeDimensions`-only bucket — grouped by\n // just above, and therefore present in `row` — reaches the caller instead\n // of being silently dropped (#4033).\n const mappedRows = rows.map(row => {\n const mapped: Record<string, unknown> = {};\n for (const dim of this.projectedDimensions(query)) {\n const shortName = this.resolveFieldName(cube, dim, 'dimension');\n if (shortName in row) mapped[dim] = row[shortName];\n }\n if (query.measures) {\n for (const m of query.measures) {\n // Alias was set to the full measure name\n if (m in row) mapped[m] = row[m];\n }\n }\n return mapped;\n });\n\n const fields = this.buildFieldMeta(query, cube);\n // Echo a representative SQL alongside the rows (#3588). `NativeSQLStrategy`\n // returns the statement it actually ran, and dataset responses surface that\n // string — it is how an author checks what their widget compiled to. This\n // path builds an AST, so it had nothing to echo, and the `sql` field simply\n // vanished from the response whenever a query was date-bucketed (native SQL\n // declines granularity, handing those queries here). An author reading the\n // response then couldn't tell \"bucketing is not implemented\" from \"this\n // strategy doesn't report\". Best-effort: rendering is a debugging aid and\n // must never fail a query that already ran.\n let sql: string | undefined;\n try {\n sql = (await this.generateSql(query, ctx)).sql;\n } catch {\n sql = undefined;\n }\n return sql ? { rows: mappedRows, fields, sql } : { rows: mappedRows, fields };\n }\n\n /**\n * Render a REPRESENTATIVE SQL string for an ObjectQL aggregate query.\n *\n * This path executes through `engine.aggregate()`, not raw SQL, so the string\n * is documentation rather than the literal statement — but it must be an\n * honest account of what the query does, because dataset responses echo it\n * and authors read it to verify their widget options landed (#3588). It\n * therefore renders date bucketing (`date_trunc`), the WHERE predicate,\n * ordering, and the row window.\n *\n * Filter VALUES are rendered as `$n` placeholders and returned in `params`,\n * never inlined: the echoed statement travels to the browser, and a filter\n * comparand can carry tenant data.\n */\n async generateSql(query: AnalyticsQuery, ctx: StrategyContext): Promise<{ sql: string; params: unknown[] }> {\n const cube = ctx.getCube(query.cube!);\n if (!cube) {\n throw new Error(`Cube not found: ${query.cube}`);\n }\n\n const selectParts: string[] = [];\n const groupByParts: string[] = [];\n const params: unknown[] = [];\n\n // Date-bucketed dimensions render as `date_trunc('<granularity>', col)` —\n // the SQL shape the driver's own bucketing implements — so a `month` trend\n // no longer reads as if it grouped by the raw column.\n const granByDim = new Map<string, string>();\n for (const td of query.timeDimensions ?? []) {\n if (td.granularity) granByDim.set(td.dimension, td.granularity);\n }\n const tableName = this.extractObjectName(cube);\n // #3654 — plan cross-object dims (throws for out-of-envelope, so\n // `/analytics/sql` and `execute()` accept/reject the SAME set). An in-envelope\n // cross-object dim renders as a LEFT JOIN — its logical shape; `execute()`\n // serves it via FK-expand.\n // EVERY member the filter touches, including ones nested in an `$or` —\n // the envelope check rejects cross-object filters, so a member it cannot\n // see is a filter it cannot reject.\n const plan = this.planCrossObject(cube, query, Object.fromEntries(\n collectFilterLeaves(normalizeAnalyticsFilterTree(query))\n .map((f) => [this.resolveFieldName(cube, f.member, 'any'), true]),\n ));\n const crossByDim = new Map((plan?.crossDims ?? []).map((cd) => [cd.outputName, cd]));\n const joinClauses: string[] = [];\n const dimExpr = (dim: string): string => {\n const cd = crossByDim.get(dim);\n if (cd) {\n joinClauses.push(\n `LEFT JOIN \"${cd.refObject}\" ON \"${tableName}\".\"${cd.fkField}\" = \"${cd.refObject}\".\"id\"`,\n );\n return `\"${cd.refObject}\".\"${cd.attr}\"`;\n }\n const col = this.resolveFieldName(cube, dim, 'dimension');\n const gran = granByDim.get(dim);\n return gran ? `date_trunc('${gran}', ${col})` : col;\n };\n\n if (query.dimensions) {\n for (const dim of query.dimensions) {\n const expr = dimExpr(dim);\n selectParts.push(`${expr} AS \"${dim}\"`);\n groupByParts.push(expr);\n }\n }\n // A time dimension that is bucketed but not also listed in `dimensions`\n // still groups (see `execute`), so it belongs in the rendered GROUP BY too.\n for (const [dim] of granByDim) {\n if (query.dimensions?.includes(dim)) continue;\n const expr = dimExpr(dim);\n selectParts.push(`${expr} AS \"${dim}\"`);\n groupByParts.push(expr);\n }\n if (query.measures) {\n for (const m of query.measures) {\n const { field, method } = this.resolveMeasureAggregation(cube, m);\n const aggSql = method === 'count'\n ? 'COUNT(*)'\n : method === 'count_distinct'\n ? `COUNT(DISTINCT ${field})`\n : `${method.toUpperCase()}(${field})`;\n selectParts.push(`${aggSql} AS \"${m}\"`);\n }\n }\n\n // ADR-0021 D-C (#3602) — render the READ SCOPE too, not just the caller's\n // own filters (#3652 added those). Without it this string still reads as an\n // unscoped table scan while the real aggregate is scoped (#3601), so anyone\n // debugging a \"why is this row missing\" gets SQL that cannot reproduce the\n // result. Nothing leaks — the string is never executed, and scope VALUES\n // stay in `params`, which `execute()`'s echo discards — but a rendering\n // that contradicts execution is worse than no rendering.\n //\n // The cross-object guard runs here for the same reason: this must not\n // render SQL for a query `execute()` would reject outright (#3654).\n //\n // Faithfulness cuts both ways: the time-dimension WINDOWS render too, from\n // the same `dateRangeBounds` lowering `execute()` sends to the engine\n // (#3650). This comment used to explain why a BETWEEN was deliberately\n // absent — because `execute()` dropped the window and rendering one would\n // have invented a predicate. Now that it applies the window, omitting it\n // here would be the lie in the other direction.\n // (The cross-object envelope was already enforced by `planCrossObject` above,\n // so `/analytics/sql` rejects the same out-of-envelope set `execute()` does.)\n\n const whereParts: string[] = [];\n // Recursive, so the echoed statement carries the same disjunctions the\n // engine filter does — the echo exists to REPRODUCE execution, and an\n // `$or` rendered as a conjunction (or dropped) is exactly the lie this\n // block's comment above warns about, in the other direction.\n const filterClause = this.renderFilterNodeSql(\n normalizeAnalyticsFilterTree(query),\n cube,\n params,\n );\n if (filterClause) whereParts.push(filterClause);\n // Bounds bind as `$n` placeholders like every other comparand: this string\n // travels to the browser, and a window can carry tenant-derived dates.\n // A bare-day upper bound renders half-open (`< day+1`) because that is\n // what `execute()`'s driver actually runs for it on a datetime column\n // (#3777) — rendering the BETWEEN would hand a debugger SQL that drops\n // the final day's rows and cannot reproduce the result.\n for (const { field, bounds } of this.dateRangeBounds(cube, query)) {\n const nextDay = nextUtcCalendarDay(bounds.$lte);\n params.push(bounds.$gte, nextDay ?? bounds.$lte);\n whereParts.push(\n `(${field} >= $${params.length - 1} AND ${field} ${nextDay ? '<' : '<='} $${params.length})`,\n );\n }\n // Read scope last, so it reads as the outermost constraint. Compiled by the\n // same fail-closed compiler `NativeSQLStrategy` uses — it throws rather than\n // drop a predicate, which is the correct posture even for a display string:\n // silently omitting the scope is exactly the misleading output being fixed.\n const scope = ctx.getReadScope?.(tableName);\n if (scope != null) {\n const { sql: scopeSql, params: scopeParams } = compileScopedFilterToSql(scope, tableName);\n if (scopeSql) {\n let i = 0;\n // `compileScopedFilterToSql` emits `?`; renumber into this builder's $N.\n const rendered = scopeSql.replace(/\\?/g, () => {\n params.push(scopeParams[i++]);\n return `$${params.length}`;\n });\n whereParts.push(`(${rendered})`);\n }\n }\n\n let sql = `SELECT ${selectParts.join(', ')} FROM \"${tableName}\"`;\n if (joinClauses.length > 0) sql += ' ' + joinClauses.join(' ');\n if (whereParts.length > 0) {\n sql += ` WHERE ${whereParts.join(' AND ')}`;\n }\n if (groupByParts.length > 0) {\n sql += ` GROUP BY ${groupByParts.join(', ')}`;\n }\n if (query.order && Object.keys(query.order).length > 0) {\n const orderClauses = Object.entries(query.order).map(([f, d]) => `\"${f}\" ${d.toUpperCase()}`);\n sql += ` ORDER BY ${orderClauses.join(', ')}`;\n }\n if (query.limit != null) sql += ` LIMIT ${query.limit}`;\n if (query.offset != null) sql += ` OFFSET ${query.offset}`;\n\n return { sql, params };\n }\n\n // ── Helpers ──────────────────────────────────────────────────────\n\n /**\n * ADR-0021 D-C (#3597) — AND the object's read scope (tenant + RLS) into the\n * filter handed to `engine.aggregate`.\n *\n * This path used to drop the scope entirely, and the engine could not make up\n * for it: the aggregate bridge passes no `ExecutionContext`, so the security\n * middleware's principal-less fall-open skipped its own RLS injection. Both\n * belts were off at once — an authenticated caller received aggregates\n * computed over EVERY tenant's rows.\n *\n * Composed with `$and`, never by key merge: the query's own filter and the\n * scope can name the SAME field (e.g. a dashboard filtering `organization_id`),\n * and a spread would let caller input silently overwrite the security\n * predicate. `$and` makes that structurally impossible.\n */\n private withReadScope(\n objectName: string,\n filter: Record<string, unknown>,\n ctx: StrategyContext,\n ): Record<string, unknown> | undefined {\n const userFilter = Object.keys(filter).length > 0 ? filter : undefined;\n if (typeof ctx.getReadScope !== 'function') return userFilter;\n const scope = ctx.getReadScope(objectName);\n if (scope === undefined || scope === null) return userFilter;\n const scopeFilter = scope as Record<string, unknown>;\n if (!userFilter) return scopeFilter;\n return { $and: [userFilter, scopeFilter] };\n }\n\n /** Is `field` a resolved cross-object (relationship-traversal) reference? */\n private isCrossObjectField(cube: Cube, field: string, baseObject: string): boolean {\n if (!field.includes('.')) return false;\n const alias = field.split('.')[0];\n const joinedObject = cube.joins?.[alias]?.name ?? alias;\n return joinedObject !== baseObject;\n }\n\n /**\n * Plan how to serve cross-object references on this join-less path (#3654).\n *\n * `engine.aggregate()` cannot join. A cross-object DIMENSION within a\n * supported envelope is served by an FK-expand (`executeCrossObject`): group\n * the base aggregate on the lookup FK, resolve the FK to the related attribute\n * with a SCOPED read, re-bucket in memory. Returns `null` for a base-only\n * query (direct path), a plan for an in-envelope cross-object query.\n *\n * THROWS for anything outside the envelope — a cross-object MEASURE or FILTER\n * (needs a real join to evaluate), a MULTI-HOP dimension (`a.b.c`), or a\n * non-recombinable measure (`avg`/`count_distinct`, whose sub-bucket values\n * cannot be merged). A loud error beats the silent mis-bucket #3654 kills.\n * `generateSql()` calls this too, so the preview accepts/rejects the same set.\n *\n * Detection is on RESOLVED field names, so a dotted dimension the cube\n * flattens to a real column is treated as base, not cross-object.\n */\n private planCrossObject(\n cube: Cube,\n query: AnalyticsQuery,\n filter: Record<string, unknown>,\n ): CrossObjectPlan | null {\n const baseObject = this.extractObjectName(cube);\n\n // A date bucket over a related object's field is not supported. Checked\n // FIRST: since #3650 a `dateRange` also lands in `filter`, so a cross-object\n // time dimension would otherwise be reported as a \"cross-object filter\" —\n // true of the lowered predicate, but not what the author wrote.\n for (const td of query.timeDimensions ?? []) {\n const field = this.resolveFieldName(cube, td.dimension, 'dimension');\n if (this.isCrossObjectField(cube, field, baseObject)) {\n throw new Error(\n `[Analytics] ObjectQLStrategy cannot bucket a cross-object time dimension (\"${field}\").`,\n );\n }\n }\n\n // A cross-object MEASURE or FILTER can only be evaluated with a real join.\n const nonDim = [\n ...(query.measures ?? []).map((m) => ({ where: 'measure', field: this.resolveMeasureAggregation(cube, m).field })),\n ...Object.keys(filter).map((f) => ({ where: 'filter', field: f })),\n ].filter((r) => this.isCrossObjectField(cube, r.field, baseObject));\n if (nonDim.length > 0) {\n throw new Error(\n `[Analytics] ObjectQLStrategy cannot evaluate a cross-object ${nonDim[0].where} ` +\n `(\"${nonDim[0].field}\") — the engine cannot join in an aggregate. Run this ` +\n `query on a native-SQL driver, or remove the cross-object ${nonDim[0].where}.`,\n );\n }\n\n // Collect cross-object DIMENSIONS (single-hop only).\n const crossDims: CrossObjectPlanDim[] = [];\n for (const dim of query.dimensions ?? []) {\n const field = this.resolveFieldName(cube, dim, 'dimension');\n if (!this.isCrossObjectField(cube, field, baseObject)) continue;\n const [alias, ...rest] = field.split('.');\n const attr = rest.join('.');\n if (attr.includes('.')) {\n throw new Error(\n `[Analytics] ObjectQLStrategy supports only single-hop cross-object ` +\n `dimensions; \"${field}\" traverses more than one relationship.`,\n );\n }\n crossDims.push({ outputName: dim, fkField: alias, attr, refObject: cube.joins?.[alias]?.name ?? alias });\n }\n\n if (crossDims.length === 0) return null;\n\n // Every measure must re-combine across the intermediate FK sub-buckets.\n for (const m of query.measures ?? []) {\n const { method } = this.resolveMeasureAggregation(cube, m);\n if (!RECOMBINABLE_METHODS.has(method)) {\n throw new Error(\n `[Analytics] ObjectQLStrategy cannot group by a cross-object dimension ` +\n `with a \"${method}\" measure (\"${m}\") — its value cannot be recombined ` +\n `across the intermediate FK grouping. Use sum/count/min/max, or run on ` +\n `a native-SQL driver.`,\n );\n }\n }\n\n return { crossDims };\n }\n\n /**\n * Serve a cross-object-dimension query by FK-expand (#3654). The pure\n * re-bucketing step lives in `cross-object-rebucket.ts`.\n */\n private async executeCrossObject(\n cube: Cube,\n query: AnalyticsQuery,\n aggregations: Array<{ field: string; method: string; alias: string }>,\n filter: Record<string, unknown>,\n plan: CrossObjectPlan,\n ctx: StrategyContext,\n ): Promise<AnalyticsResult> {\n const baseObject = this.extractObjectName(cube);\n const crossByDim = new Map(plan.crossDims.map((cd) => [cd.outputName, cd]));\n\n // Rewrite group-by: a cross-object dim becomes its base FK column; base and\n // time dims pass through. `baseDimFields` are the group keys carried into\n // the re-bucket verbatim (the FK columns are replaced by resolved attrs).\n type GroupByItem = string | { field: string; dateGranularity: string };\n const granByDim = new Map<string, string>();\n for (const td of query.timeDimensions ?? []) {\n if (td.granularity) granByDim.set(td.dimension, td.granularity);\n }\n const groupBy: GroupByItem[] = [];\n const baseDimFields: string[] = [];\n for (const dim of query.dimensions ?? []) {\n const cd = crossByDim.get(dim);\n if (cd) {\n groupBy.push(cd.fkField);\n continue;\n }\n const field = this.resolveFieldName(cube, dim, 'dimension');\n const gran = granByDim.get(dim);\n groupBy.push(gran ? { field, dateGranularity: gran } : field);\n baseDimFields.push(field);\n granByDim.delete(dim);\n }\n for (const [dim, gran] of granByDim) {\n const field = this.resolveFieldName(cube, dim, 'dimension');\n groupBy.push({ field, dateGranularity: gran });\n baseDimFields.push(field);\n }\n\n // Base aggregate, grouped by the FK, scoped to the base object. Threads the\n // ExecutionContext for the engine-side second belt too (#3602).\n const baseRows = await ctx.executeAggregate!(baseObject, {\n groupBy: groupBy.length > 0 ? (groupBy as unknown as string[]) : undefined,\n aggregations: aggregations.length > 0 ? aggregations : undefined,\n filter: this.withReadScope(baseObject, filter, ctx),\n timezone: query.timezone,\n context: ctx.context,\n });\n\n // Resolve each cross-object dim's FK → attribute, SCOPED to the referenced\n // object: a related record the caller cannot read never yields its\n // attribute, so it buckets as RESTRICTED (no leak; ADR-0021 D-C / #3602).\n const resolvedDims: CrossObjectDim[] = [];\n for (const cd of plan.crossDims) {\n const fkValues = [...new Set(baseRows.map((r) => r[cd.fkField]).filter((v) => v != null))];\n const fkToAttr = await this.resolveFkAttr(cd.refObject, cd.attr, fkValues, ctx);\n resolvedDims.push({ outputName: cd.outputName, fkField: cd.fkField, fkToAttr });\n }\n\n const measures: MeasureRecombine[] = (query.measures ?? []).map((m) => ({\n alias: m,\n // planCrossObject already asserted every measure is recombinable.\n method: this.resolveMeasureAggregation(cube, m).method as RecombinableMethod,\n }));\n\n const merged = rebucketCrossObject(baseRows, baseDimFields, resolvedDims, measures);\n\n // Map resolved group keys back to the caller's dimension names.\n const mappedRows = merged.map((row) => {\n const out: Record<string, unknown> = {};\n // Same projection set as the direct path and `buildFieldMeta`\n // ({@link projectedDimensions}) — a cross-object dimension carries the\n // caller's name already, everything else is remapped from its short name.\n for (const dim of this.projectedDimensions(query)) {\n if (crossByDim.has(dim)) {\n if (dim in row) out[dim] = row[dim];\n } else {\n const field = this.resolveFieldName(cube, dim, 'dimension');\n if (field in row) out[dim] = row[field];\n }\n }\n for (const m of query.measures ?? []) {\n if (m in row) out[m] = row[m];\n }\n return out;\n });\n\n return { rows: mappedRows, fields: this.buildFieldMeta(query, cube) };\n }\n\n /**\n * Resolve `fkValues` (ids of `refObject`) to their `attr` values, applying the\n * referenced object's OWN read scope (#3654 / #3602). Reuses the aggregate\n * bridge — `group by (id, attr)` is one row per record. Ids the scope hides\n * are simply absent from the map (⇒ RESTRICTED bucket downstream).\n */\n private async resolveFkAttr(\n refObject: string,\n attr: string,\n fkValues: unknown[],\n ctx: StrategyContext,\n ): Promise<Map<unknown, unknown>> {\n const map = new Map<unknown, unknown>();\n if (fkValues.length === 0 || typeof ctx.executeAggregate !== 'function') return map;\n const idFilter: Record<string, unknown> = { id: { $in: fkValues } };\n const scope = typeof ctx.getReadScope === 'function' ? ctx.getReadScope(refObject) : null;\n const filter = scope != null ? { $and: [idFilter, scope] } : idFilter;\n const rows = await ctx.executeAggregate(refObject, {\n groupBy: ['id', attr],\n aggregations: [{ field: 'id', method: 'count', alias: '_c' }],\n filter,\n context: ctx.context,\n });\n for (const r of rows) {\n if (r.id != null) map.set(r.id, r[attr]);\n }\n return map;\n }\n\n /**\n * Render one normalized filter as a display SQL predicate for `generateSql`.\n *\n * Mirrors `NativeSQLStrategy.buildFilterClause`'s operator vocabulary so the\n * two previews read alike, but binds through `coerceFilterValueForObjectQL`:\n * the comparand shown is the one THIS path actually hands the engine (a real\n * boolean, not SQL's 1/0). Returns null for an operator/value combination\n * that carries no predicate, matching `execute()`, which drops it too.\n */\n private buildFilterClauseSql(\n col: string,\n operator: string,\n values: string[] | undefined,\n params: unknown[],\n ): string | null {\n if (operator === 'set') return `${col} IS NOT NULL`;\n if (operator === 'notSet') return `${col} IS NULL`;\n\n if (!values || values.length === 0) return null;\n\n if (operator === 'in' || operator === 'notIn') {\n const placeholders = values\n .map((v) => { params.push(coerceFilterValueForObjectQL(v)); return `$${params.length}`; })\n .join(', ');\n return `${col} ${operator === 'in' ? 'IN' : 'NOT IN'} (${placeholders})`;\n }\n\n if (operator === 'contains' || operator === 'notContains') {\n params.push(`%${values[0]}%`);\n return `${col} ${operator === 'contains' ? 'LIKE' : 'NOT LIKE'} $${params.length}`;\n }\n\n const op = SCALAR_SQL_OPS[operator];\n if (!op) return null;\n params.push(coerceFilterValueForObjectQL(values[0]));\n return `${col} ${op} $${params.length}`;\n }\n\n /**\n * Resolve a member ref to a `{ sql, type? }` definition.\n *\n * Mirrors `NativeSQLStrategy.lookupMember` so the two strategies\n * accept the same naming conventions:\n * 1. `<cube>.<field>` — canonical analytics qualifier.\n * 2. `<lookup>.<field>` — relation traversal (e.g. `account.industry`).\n * Tries literal key, then underscore-flattened key, then falls\n * back to a synthetic dim whose `sql` is the dotted path so the\n * ObjectQL aggregate engine can traverse it via the lookup field.\n * 3. `<field>` — bare column on the cube's table.\n */\n private lookupMember(\n cube: Cube,\n member: string,\n kind: 'dimension' | 'measure',\n ): { sql: string; type?: string } | undefined {\n const bag = kind === 'dimension' ? cube.dimensions : cube.measures;\n if (bag[member]) return bag[member];\n if (member.includes('.')) {\n const [first, ...rest] = member.split('.');\n const tail = rest.join('.');\n if (first === cube.name && bag[tail]) return bag[tail];\n if (bag[tail]) return bag[tail];\n const flat = member.replace(/\\./g, '_');\n if (bag[flat]) return bag[flat];\n if (kind === 'dimension') return { sql: member, type: 'string' };\n } else if (bag[member]) {\n return bag[member];\n }\n return undefined;\n }\n\n private resolveFieldName(cube: Cube, member: string, kind: 'dimension' | 'measure' | 'any'): string {\n if (kind === 'dimension' || kind === 'any') {\n const dim = this.lookupMember(cube, member, 'dimension');\n if (dim) return dim.sql.replace(/^\\$/, '');\n }\n if (kind === 'measure' || kind === 'any') {\n const measure = this.lookupMember(cube, member, 'measure');\n if (measure) return measure.sql.replace(/^\\$/, '');\n }\n return member.includes('.') ? member.split('.')[1] : member;\n }\n\n private resolveMeasureAggregation(cube: Cube, measureName: string): { field: string; method: string } {\n const direct = this.lookupMember(cube, measureName, 'measure') as\n | { sql: string; type: string }\n | undefined;\n if (direct) {\n return {\n field: direct.sql.replace(/^\\$/, ''),\n method: direct.type === 'count_distinct' ? 'count_distinct' : direct.type,\n };\n }\n // Accept `${field}_${type}` aliases (e.g. 'amount_sum') for measures whose\n // canonical name is just `${field}` (e.g. measure 'amount' of type 'sum').\n // This matches the convention used by clients that build measure names\n // from (field, function) pairs (e.g. the data-objectstack adapter).\n const fieldName = measureName.includes('.') ? measureName.split('.')[1] : measureName;\n const aggTypes = ['count', 'sum', 'avg', 'min', 'max', 'count_distinct'];\n for (const type of aggTypes) {\n const suffix = `_${type}`;\n if (fieldName.endsWith(suffix)) {\n const baseField = fieldName.slice(0, -suffix.length);\n const candidate = cube.measures[baseField];\n if (candidate && candidate.type === type) {\n return {\n field: candidate.sql.replace(/^\\$/, ''),\n method: candidate.type === 'count_distinct' ? 'count_distinct' : candidate.type,\n };\n }\n }\n }\n return { field: '*', method: 'count' };\n }\n\n /**\n * AND one more operand onto `filter[field]`, merging operator objects rather\n * than overwriting them. Returns a standalone conjunct when the two cannot\n * share one entry, or `null` when the merge absorbed the operand.\n *\n * Every predicate this strategy contributes goes through here — the caller's\n * `where` and the time-dimension `dateRange` alike. Two operands on one field\n * are the normal case (`{$gte}` from a `where` plus `{$gte,$lte}` from a\n * window on `close_date`), and a plain assignment would keep only the last:\n * that is how a range used to lose a bound.\n *\n * Spreading is sound only while the operands name DIFFERENT operators. Where\n * they collide — two `$gte` bounds on one field, which a window makes routine\n * and which a `where` can already produce on its own through `$and` — the\n * spread keeps whichever came last and WIDENS the query. Same for a bare\n * equality meeting an operator object: neither can absorb the other. Those\n * are handed back for the caller to AND in separately, so the engine\n * intersects them instead of the strategy picking a winner.\n */\n /**\n * Fold a normalized filter node into the engine filter being built.\n *\n * AND-ed LEAVES merge per field through {@link mergeFilterOperand}, exactly\n * as the flat loop this replaced did — so a query without combinators still\n * produces byte-identical engine input. Anything structural (`$or`, `$not`,\n * a nested `$and` that cannot merge) becomes its own conjunct, which the\n * caller ANDs in. The engine speaks these combinators natively\n * (`FilterCondition` declares them and every driver compiles them), so this\n * path hands them over rather than lowering them.\n */\n private applyFilterNode(\n node: NormalizedFilterNode | null,\n cube: Cube,\n filter: Record<string, unknown>,\n conjuncts: Record<string, unknown>[],\n ): void {\n if (!node) return;\n\n if (node.kind === 'leaf') {\n const fieldName = this.resolveFieldName(cube, node.member, 'any');\n const extra = this.mergeFilterOperand(filter, fieldName, this.convertFilter(node.operator, node.values));\n if (extra) conjuncts.push(extra);\n return;\n }\n\n if (node.kind === 'and') {\n for (const child of node.children) this.applyFilterNode(child, cube, filter, conjuncts);\n return;\n }\n\n const rendered = this.filterNodeToCondition(node, cube);\n if (rendered) conjuncts.push(rendered);\n }\n\n /** A node as a standalone `FilterCondition` the engine can consume. */\n private filterNodeToCondition(\n node: NormalizedFilterNode | null,\n cube: Cube,\n ): Record<string, unknown> | null {\n if (!node) return null;\n\n if (node.kind === 'not') {\n const inner = this.filterNodeToCondition(node.child, cube);\n return inner ? { $not: inner } : null;\n }\n\n if (node.kind === 'or') {\n const branches = node.children\n .map((child) => this.filterNodeToCondition(child, cube))\n .filter((c): c is Record<string, unknown> => !!c);\n return branches.length > 0 ? { $or: branches } : null;\n }\n\n // `leaf` and `and` share the merge path so one field carrying several\n // operators composes here the same way it does at the top level.\n const filter: Record<string, unknown> = {};\n const conjuncts: Record<string, unknown>[] = [];\n this.applyFilterNode(node, cube, filter, conjuncts);\n if (conjuncts.length > 0) {\n filter.$and = [...(Array.isArray(filter.$and) ? filter.$and : []), ...conjuncts];\n }\n return Object.keys(filter).length > 0 ? filter : null;\n }\n\n /**\n * Render a normalized filter node as the display SQL `/analytics/sql`\n * echoes. Values still bind as `$n` placeholders — the echo travels to the\n * browser, so a comparand is never inlined.\n */\n private renderFilterNodeSql(\n node: NormalizedFilterNode | null,\n cube: Cube,\n params: unknown[],\n ): string | null {\n if (!node) return null;\n\n if (node.kind === 'leaf') {\n return this.buildFilterClauseSql(\n this.resolveFieldName(cube, node.member, 'any'),\n node.operator,\n node.values,\n params,\n );\n }\n\n if (node.kind === 'not') {\n const inner = this.renderFilterNodeSql(node.child, cube, params);\n return inner ? `NOT (${inner})` : null;\n }\n\n const parts = node.children\n .map((child) => this.renderFilterNodeSql(child, cube, params))\n .filter((s): s is string => !!s);\n if (parts.length === 0) return null;\n if (parts.length === 1) return parts[0];\n return `(${parts.join(node.kind === 'or' ? ' OR ' : ' AND ')})`;\n }\n\n private mergeFilterOperand(\n filter: Record<string, unknown>,\n field: string,\n operand: unknown,\n ): Record<string, unknown> | null {\n const existing = filter[field];\n if (existing === undefined) {\n filter[field] = operand;\n return null;\n }\n const mergeable = (v: unknown): v is Record<string, unknown> =>\n !!v && typeof v === 'object' && !Array.isArray(v);\n if (!mergeable(existing) || !mergeable(operand)) return { [field]: operand };\n if (Object.keys(operand).some((op) => op in existing)) return { [field]: operand };\n filter[field] = { ...existing, ...operand };\n return null;\n }\n\n /**\n * Lower `timeDimensions[].dateRange` into resolved-field bounds (#3650).\n *\n * `dateRange` states a WINDOW on a time dimension; it is a SIBLING of `where`,\n * never folded into it. `normalizeAnalyticsFilters` reads only `where`, so\n * this path used to drop the window on the floor — no error, just every row\n * ever recorded. Nor is that a corner case: `NativeSQLStrategy.canHandle`\n * declines any query carrying a `granularity`, so a date-bucketed trend lands\n * HERE on every driver — and \"bucketed trend\" is precisely the shape that also\n * carries a range (\"last 12 months\", \"this quarter\").\n *\n * Bounds are inclusive on both ends — logically \"from day X through day Y\".\n * The `$lte` end is left as the bare calendar day on purpose: the driver's\n * filter compiler owns the calendar-day → instant translation, compiling a\n * bare-day `$lte` on a `datetime` column into the half-open `< nextDay`\n * (#3777) while a `date` column keeps the plain `<=`. `NativeSQLStrategy`\n * performs the same half-open translation itself because it binds into raw\n * SQL, so one dashboard reads the same on every driver.\n *\n * Comparands are coerced by the SAME helper the `where` path uses, so an\n * epoch-ms bound recovers as a number and an ISO string stays a string. No\n * STORAGE coercion happens here, deliberately: `NativeSQLStrategy` needs\n * `coerceTemporal` because it binds into raw SQL and had to learn that a\n * SQLite `Field.datetime` is an INTEGER epoch (#2034); this path goes through\n * `engine.aggregate()`, where the driver's own CRUD filter coercion applies —\n * the very coercion that already makes a `where` bound on that same column\n * work today.\n *\n * A bare-string `dateRange` degenerates to the single point `[s, s]`, matching\n * `NativeSQLStrategy`. Relative phrases (\"Last 7 days\") are NOT resolved here;\n * neither SQL path resolves them, and inventing a second interpretation on the\n * driver-independent path is how the two would drift apart again.\n *\n * An oddly-sized array (the schema types `dateRange` as a plain `string[]`)\n * takes its first two entries, a one-entry array degenerating to a point.\n * `NativeSQLStrategy` drops such a window entirely — but \"drop the window\"\n * means \"plot all of history\", which is the very failure this fixes, so the\n * fallback here errs toward the narrower query instead.\n */\n private dateRangeBounds(\n cube: Cube,\n query: AnalyticsQuery,\n ): Array<{ field: string; bounds: Record<string, unknown> }> {\n const out: Array<{ field: string; bounds: Record<string, unknown> }> = [];\n for (const td of query.timeDimensions ?? []) {\n if (!td.dateRange) continue;\n const range = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];\n const [start, end = start] = range;\n if (start == null) continue;\n out.push({\n field: this.resolveFieldName(cube, td.dimension, 'dimension'),\n bounds: {\n $gte: coerceFilterValueForObjectQL(String(start)),\n $lte: coerceFilterValueForObjectQL(String(end)),\n },\n });\n }\n return out;\n }\n\n private convertFilter(operator: string, values?: string[]): unknown {\n if (operator === 'set') return { $ne: null };\n if (operator === 'notSet') return null;\n if (!values || values.length === 0) return undefined;\n\n const v0 = coerceFilterValueForObjectQL(values[0]);\n const all = values.map(coerceFilterValueForObjectQL);\n switch (operator) {\n case 'equals': return v0;\n case 'notEquals': return { $ne: v0 };\n case 'gt': return { $gt: v0 };\n case 'gte': return { $gte: v0 };\n case 'lt': return { $lt: v0 };\n case 'lte': return { $lte: v0 };\n case 'contains': return { $regex: values[0] };\n // `notContains` had no arm and fell to the `default` below, which returns\n // a BARE VALUE — i.e. `{field: 'x'}`, an equality. \"does not contain x\"\n // was compiled as \"equals x\". These three pass through as the canonical\n // spec operators every driver implements directly, so an anchored match\n // stays anchored rather than depending on regex dialect (#4128).\n case 'notContains': return { $notContains: values[0] };\n case 'startsWith': return { $startsWith: values[0] };\n case 'endsWith': return { $endsWith: values[0] };\n case 'in': return { $in: all };\n case 'notIn': return { $nin: all };\n default:\n // Was `return v0` — a silent reinterpretation of the operator as an\n // equality, the write-side twin of the normalizer's dropped predicate\n // (#4128). Every operator `normalizeAnalyticsFilters` can emit is\n // handled above, so reaching here means the two drifted apart.\n throw new Error(\n `[analytics] ObjectQL strategy cannot express filter operator \"${operator}\". ` +\n `Treating it as an equality would silently query something the author did not ask for.`,\n );\n }\n }\n\n private extractObjectName(cube: Cube): string {\n return cube.sql.trim();\n }\n\n /**\n * The dimensions this query PROJECTS, in the order the result carries them:\n * every `dimensions` entry, then every granular `timeDimensions` entry that\n * is not already one of them.\n *\n * `timeDimensions` is not merely a filter carrier. An entry with a\n * `granularity` is GROUPED BY — see the `td.granularity` sites that build\n * groupBy here, in `generateSql` and in the cross-object path — so its\n * bucket is a COLUMN of the result; an entry without one only contributes a\n * `dateRange` predicate and must NOT be projected.\n *\n * Grouping, row mapping and {@link buildFieldMeta} have to agree on exactly\n * that set. When they did not, a bucketed query returned rows carrying only\n * the measures and a `fields` list that never mentioned the bucket — a trend\n * chart got N values and no x-axis (#4033) — even though the SQL had\n * selected `date_trunc(…) AS \"<dim>\"` all along. One definition, every\n * consumer.\n */\n private projectedDimensions(query: AnalyticsQuery): string[] {\n const out = [...(query.dimensions ?? [])];\n for (const td of query.timeDimensions ?? []) {\n if (td.granularity && !out.includes(td.dimension)) out.push(td.dimension);\n }\n return out;\n }\n\n private buildFieldMeta(query: AnalyticsQuery, cube: Cube): Array<{ name: string; type: string }> {\n const fields: Array<{ name: string; type: string }> = [];\n for (const dim of this.projectedDimensions(query)) {\n const d = this.lookupMember(cube, dim, 'dimension');\n fields.push({ name: dim, type: d?.type || 'string' });\n }\n if (query.measures) {\n for (const m of query.measures) {\n fields.push({ name: m, type: 'number' });\n }\n }\n return fields;\n }\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Cross-object dimension re-bucketing (#3654 capability).\n *\n * `engine.aggregate()` cannot join, so the ObjectQL path cannot group directly\n * by a related object's attribute (`account.region`). Instead the strategy:\n * 1. groups the base aggregate by the LOOKUP FK column (`account`), which the\n * engine CAN do (it is a plain base column), and\n * 2. resolves each FK id to the related attribute (`region`) with a SCOPED\n * read of the referenced object, then\n * 3. re-buckets the base aggregate by that attribute here, in memory,\n * recombining the measures.\n *\n * This module is the pure, deterministic step (3): given the base rows, the\n * FK→attribute maps, and the measures' aggregation methods, produce the rows a\n * direct cross-object grouping would have. It is unit-tested in isolation\n * because a wrong re-combination silently corrupts totals — exactly the class of\n * bug #3654 exists to kill.\n *\n * A base row whose FK does not resolve (the referenced record is hidden by the\n * referenced object's own RLS) buckets under {@link RESTRICTED_BUCKET}: its\n * measure still counts, so grand totals are preserved, but the hidden record's\n * attribute value never appears (no leak — ADR-0021 D-C, the #3602 class).\n */\n\n/** Only aggregation methods that re-combine across sub-buckets are supported. */\nexport type RecombinableMethod = 'sum' | 'count' | 'min' | 'max';\n\nexport const RECOMBINABLE_METHODS: ReadonlySet<string> = new Set<RecombinableMethod>([\n 'sum',\n 'count',\n 'min',\n 'max',\n]);\n\n/** Sentinel bucket for base rows whose referenced record the caller cannot read. */\nexport const RESTRICTED_BUCKET = '(restricted)';\n\nexport interface CrossObjectDim {\n /** Output key for the resolved attribute (the original dimension name), e.g. `region`. */\n outputName: string;\n /** The base FK column the base aggregate was grouped by, e.g. `account`. */\n fkField: string;\n /** `fkValue → attributeValue`. An FK absent from the map buckets as RESTRICTED. */\n fkToAttr: Map<unknown, unknown>;\n}\n\nexport interface MeasureRecombine {\n /** The measure's output key in each base row. */\n alias: string;\n method: RecombinableMethod;\n}\n\n/**\n * Order two measure values. A number orders as itself; a `Date` or an ISO\n * timestamp orders as its instant, so a `min`/`max` over a temporal measure\n * compares correctly instead of collapsing to `NaN` (#3797). `NaN` means \"not\n * orderable\" and the caller keeps the other side.\n */\nfunction orderableValue(v: unknown): number {\n if (v == null) return NaN;\n if (typeof v === 'number') return v;\n if (v instanceof Date) return v.getTime();\n const n = Number(v);\n if (Number.isFinite(n)) return n;\n return Date.parse(String(v));\n}\n\n/**\n * Combine two measure values under an aggregation method (either may be\n * undefined).\n *\n * `sum`/`count` are numeric by construction and stay so. `min`/`max` return the\n * winning ORIGINAL value rather than a number: the value they pick is a value\n * OF the column, so a temporal measure has to come back out in the same shape\n * the driver presented it (#3797) — coercing it to a number here would put the\n * epoch leak back one layer up, and on any dialect whose driver returns an ISO\n * string it would produce `NaN` outright.\n */\nfunction recombine(method: RecombinableMethod, acc: unknown, next: unknown): unknown {\n if (method === 'min' || method === 'max') {\n if (acc === undefined) return next ?? 0;\n const a = orderableValue(acc);\n const n = orderableValue(next);\n if (Number.isNaN(n)) return acc;\n if (Number.isNaN(a)) return next;\n const nextWins = method === 'min' ? n < a : n > a;\n return nextWins ? next : acc;\n }\n const n = Number(next ?? 0);\n return acc === undefined ? n : Number(acc) + n;\n}\n\n/**\n * Re-bucket base aggregate rows by resolved cross-object attributes.\n *\n * @param baseRows rows grouped by `baseDimFields` + every `crossDims[*].fkField`.\n * @param baseDimFields the NON-cross-object group keys carried through unchanged\n * (base columns and date buckets), keyed as in `baseRows`.\n * @param crossDims one entry per cross-object dimension (its FK→attr map).\n * @param measures measure keys + their (recombinable) aggregation method.\n * @returns rows keyed by `baseDimFields` + each `crossDims[*].outputName` + measures.\n */\nexport function rebucketCrossObject(\n baseRows: Record<string, unknown>[],\n baseDimFields: string[],\n crossDims: CrossObjectDim[],\n measures: MeasureRecombine[],\n): Record<string, unknown>[] {\n const buckets = new Map<string, Record<string, unknown>>();\n\n for (const row of baseRows) {\n // Resolve each cross-object FK to its attribute (or RESTRICTED).\n const resolved: Record<string, unknown> = {};\n for (const cd of crossDims) {\n const fk = row[cd.fkField];\n resolved[cd.outputName] = cd.fkToAttr.has(fk) ? cd.fkToAttr.get(fk) : RESTRICTED_BUCKET;\n }\n\n // Bucket key = base dims (unchanged) + resolved attributes. `\u0001` is a\n // separator no group value contains, matching the engine's own convention.\n const keyParts: string[] = [];\n // JSON-encoded, so the empty bucket (`null` on both aggregation paths since\n // #3839) stays distinct from a row whose value is the literal string\n // `\"null\"` — plain interpolation renders both as `null` and would merge two\n // real groups into one. Only this composite id is affected; the emitted\n // bucket keeps the row's own value verbatim below.\n for (const f of baseDimFields) keyParts.push(`${f}=${JSON.stringify(row[f] ?? null)}`);\n for (const cd of crossDims) keyParts.push(`${cd.outputName}=${String(resolved[cd.outputName])}`);\n const key = keyParts.join('\u0001');\n\n let bucket = buckets.get(key);\n if (!bucket) {\n bucket = {};\n for (const f of baseDimFields) bucket[f] = row[f];\n for (const cd of crossDims) bucket[cd.outputName] = resolved[cd.outputName];\n buckets.set(key, bucket);\n }\n for (const m of measures) {\n bucket[m.alias] = recombine(m.method, bucket[m.alias], row[m.alias]);\n }\n }\n\n return [...buckets.values()];\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Cube, Metric, Dimension as CubeDimension, CubeJoin } from '@objectstack/spec/data';\nimport { AggregationFunction } from '@objectstack/spec/data';\nimport type { Dataset, DatasetMeasure, DatasetDimension } from '@objectstack/spec/ui';\nimport type { FilterCondition } from '@objectstack/spec/data';\n\n/**\n * Dataset → Cube compiler (ADR-0021 D-A=(c), WS2).\n *\n * Lowers a declarative `dataset` (base object + included relationships +\n * declared dimensions/measures + derived measures) into the existing Cube\n * analytics runtime model. The author never writes an `ON` clause: joins are\n * DERIVED from the `include` relationship names and the dotted `relationship.field`\n * references on dimensions/measures, matching the NativeSQLStrategy convention\n * `<parentTable>.<relationship> = <relationship>.id`.\n *\n * Safety (D-C): every dotted field reference must point at a relationship that\n * the dataset explicitly declared in `include`; otherwise the compile fails.\n * The returned `allowedRelationships` set is the join allowlist the strategy\n * enforces at SQL-build time.\n */\n\n/** Operators v1 does NOT compile to the Cube SQL switch — surfaced as a clear error. */\nexport const UNSUPPORTED_AGGREGATES = new Set(['array_agg', 'string_agg']);\n\n/**\n * What v1 *can* lower — derived from the spec's vocabulary rather than restated.\n *\n * The list used to be hand-written prose inside the error message below, which\n * made it a third copy of one vocabulary (after `AggregationFunction` and the\n * `native-sql-strategy` switch) with nothing keeping the three in step. An\n * aggregate added to the spec would have passed this gate, been reported as\n * supported by that message, and then hit the strategy's `default` — returning\n * a row count in place of the requested number. objectui#2945.\n */\nexport const SUPPORTED_AGGREGATES: string[] = AggregationFunction.options\n .filter((a: string) => !UNSUPPORTED_AGGREGATES.has(a));\n\nexport interface DerivedMeasureSpec {\n name: string;\n op: 'ratio' | 'sum' | 'difference' | 'product';\n of: string[];\n}\n\nexport interface CompiledDataset {\n /** The Cube the dataset compiles to (consumed by the strategy chain). */\n cube: Cube;\n /**\n * Every join alias the dataset may use — each declared `include` path AND its\n * intermediate prefixes (ADR-0071). The join allowlist (D-C): the\n * NativeSQLStrategy rejects any join alias not in this set.\n */\n allowedRelationships: Set<string>;\n /** Derived measures, computed post-aggregation by the executor (Q1). */\n derived: DerivedMeasureSpec[];\n /** Definition-level filter (the dataset's intrinsic scope). */\n filter?: FilterCondition;\n /** Per-measure scoped filters, keyed by measure name (applied by executor). */\n measureFilters: Record<string, FilterCondition>;\n}\n\n/**\n * The related object reached by traversing a relationship: its logical object\n * name (used to resolve the NEXT hop in a multi-hop chain — ADR-0071) and its\n * physical table name (the join target).\n */\nexport interface RelationshipTarget {\n object: string;\n table: string;\n}\n\n/**\n * Resolves a relationship name on a base object to the related object/table,\n * using the runtime's object graph. Optional: when omitted the compiler trusts\n * the declared `include` names (the NativeSQLStrategy convention assumes the\n * relationship name equals the related table name).\n *\n * May return a bare table-name `string` (legacy single-hop: object name is\n * assumed equal to the table) or a {@link RelationshipTarget} (required to\n * traverse further along a multi-hop path, where object differs from table for\n * namespaced objects).\n */\nexport type RelationshipResolver = (\n baseObject: string,\n relationshipName: string,\n) => string | RelationshipTarget | undefined;\n\n/** Map a dataset measure's aggregate to the Cube metric `type`. */\nfunction aggregateToMetricType(m: DatasetMeasure): Metric['type'] {\n // Only reached for non-derived measures, where the spec refinement guarantees\n // an aggregate; guard defensively so the type narrows from `optional`.\n if (!m.aggregate) {\n throw new Error(`[dataset-compiler] non-derived measure \"${m.name}\" has no aggregate`);\n }\n if (UNSUPPORTED_AGGREGATES.has(m.aggregate)) {\n throw new Error(\n `[dataset-compiler] measure \"${m.name}\" uses aggregate \"${m.aggregate}\" which is ` +\n `not supported by the v1 dataset runtime (supported: ${SUPPORTED_AGGREGATES.join(', ')}).`,\n );\n }\n return m.aggregate as Metric['type'];\n}\n\n/** Map a dataset dimension type to the Cube dimension `type`. */\nfunction dimensionType(d: DatasetDimension): CubeDimension['type'] {\n switch (d.type) {\n case 'date': return 'time';\n case 'number': return 'number';\n case 'boolean': return 'boolean';\n case 'lookup': return 'string';\n case 'string': return 'string';\n default: return 'string';\n }\n}\n\n/** The relationship PATH a dotted field traverses — all segments but the final\n * column — or null for a base-object field. E.g. `account.owner.region` →\n * `account.owner`; `account.region` → `account`; `region` → null. */\nfunction fieldRelationshipPath(field: string): string | null {\n const idx = field.lastIndexOf('.');\n return idx > 0 ? field.slice(0, idx) : null;\n}\n\n/** Max relationship hops in one `include` path — base → 3 hops = 4 objects\n * (ADR-0071; Salesforce-report-type parity). To-one chains never fan out, so\n * this is a performance/complexity guard, not a correctness limit. */\nconst MAX_JOIN_HOPS = 3;\n\n/** SQL-safe join alias for a relationship PATH. The dotted path is the author-\n * facing form; the alias replaces dots with `__` (Cube.js convention) so each\n * prefix is one valid identifier — quoted dotted identifiers are rejected by\n * the read-scope SQL guard (fail-closed). Single-segment paths are unchanged,\n * so single-hop joins stay byte-for-byte identical. */\nconst joinAlias = (path: string): string => path.replace(/\\./g, '__');\n\nexport function compileDataset(\n dataset: Dataset,\n resolver?: RelationshipResolver,\n): CompiledDataset {\n const include = dataset.include ?? [];\n\n // Resolve each declared relationship PATH into its ordered join chain, emitting\n // one Cube join per PATH PREFIX (ADR-0071 multi-hop, to-one only). The join\n // ALIAS is the full dotted path (`account.owner`), which self-describes the\n // chain: the parent alias is the path minus its last segment, the FK column is\n // that last segment. So declaring `account.owner` auto-adds the intermediate\n // `account` join, and the strategy can rebuild every `ON` from the alias alone.\n // Without a resolver, each segment's relationship name is assumed to equal both\n // the related object and its table (legacy convention / unit tests).\n const resolveHop = (fromObject: string, rel: string): RelationshipTarget => {\n if (!resolver) return { object: rel, table: rel };\n const resolved = resolver(fromObject, rel);\n if (!resolved) {\n throw new Error(\n `[dataset-compiler] dataset \"${dataset.name}\" includes relationship \"${rel}\" ` +\n `which does not exist on object \"${fromObject}\".`,\n );\n }\n return typeof resolved === 'string' ? { object: resolved, table: resolved } : resolved;\n };\n const joins: Record<string, CubeJoin> = {};\n for (const path of include) {\n const segments = path.split('.');\n if (segments.length > MAX_JOIN_HOPS) {\n throw new Error(\n `[dataset-compiler] dataset \"${dataset.name}\" include path \"${path}\" exceeds the ` +\n `${MAX_JOIN_HOPS}-hop limit (${segments.length} hops). Deeper traversal is not supported.`,\n );\n }\n let fromObject = dataset.object;\n let parentAlias = dataset.object;\n let prefix = '';\n for (const seg of segments) {\n prefix = prefix ? `${prefix}.${seg}` : seg;\n const target = resolveHop(fromObject, seg);\n const alias = joinAlias(prefix);\n if (!joins[alias]) {\n // KEY is the SQL-safe alias; `name` carries the join TABLE; the strategy\n // rebuilds the ON clause from the alias convention (`<parent>.<seg> = <alias>.id`).\n joins[alias] = {\n name: target.table,\n relationship: 'many_to_one',\n sql: `${parentAlias}.${seg} = ${prefix}.id`,\n };\n }\n fromObject = target.object;\n parentAlias = prefix;\n }\n }\n\n // The join allowlist (D-C) is every registered alias — each declared path AND\n // its intermediate prefixes — so a multi-hop field's intermediate joins pass.\n const allowedRelationships = new Set(Object.keys(joins));\n\n // Assert any dotted field only traverses a DECLARED relationship PATH (D-C).\n const assertDeclared = (field: string, ownerKind: string, ownerName: string) => {\n const relPath = fieldRelationshipPath(field);\n if (relPath && !joins[joinAlias(relPath)]) {\n throw new Error(\n `[dataset-compiler] ${ownerKind} \"${ownerName}\" references relationship path \"${relPath}\" ` +\n `via \"${field}\", but \"${relPath}\" is not declared in the dataset's \\`include\\`. ` +\n `Only fields along a declared relationship path are joinable.`,\n );\n }\n };\n\n // Compile dimensions.\n const dimensions: Record<string, CubeDimension> = {};\n for (const d of dataset.dimensions) {\n assertDeclared(d.field, 'dimension', d.name);\n const dim: CubeDimension = {\n name: d.name,\n label: typeof d.label === 'string' ? d.label : d.name,\n type: dimensionType(d),\n sql: d.field,\n };\n if (dim.type === 'time') {\n dim.granularities = d.dateGranularity\n ? [d.dateGranularity]\n : ['day', 'week', 'month', 'quarter', 'year'];\n }\n dimensions[d.name] = dim;\n }\n\n // Compile measures (non-derived → Cube metrics; derived → sidecar).\n const measures: Record<string, Metric> = {};\n const derived: DerivedMeasureSpec[] = [];\n const measureFilters: Record<string, FilterCondition> = {};\n\n for (const m of dataset.measures) {\n if (m.derived) {\n derived.push({ name: m.name, op: m.derived.op, of: m.derived.of });\n continue;\n }\n if (m.field) assertDeclared(m.field, 'measure', m.name);\n const metric: Metric = {\n name: m.name,\n label: typeof m.label === 'string' ? m.label : m.name,\n type: aggregateToMetricType(m),\n // `count` with no field aggregates over rows (*).\n sql: m.field ?? '*',\n };\n if (typeof m.format === 'string') metric.format = m.format;\n measures[m.name] = metric;\n if (m.filter) measureFilters[m.name] = m.filter;\n }\n\n const cube: Cube = {\n name: dataset.name,\n title: typeof dataset.label === 'string' ? dataset.label : dataset.name,\n sql: dataset.object,\n measures,\n dimensions,\n public: false,\n };\n if (Object.keys(joins).length > 0) cube.joins = joins;\n\n return {\n cube,\n allowedRelationships,\n derived,\n filter: dataset.filter,\n measureFilters,\n };\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n IAnalyticsService,\n AnalyticsQuery,\n AnalyticsResult,\n DatasetSelection,\n DatasetCompareTo,\n} from '@objectstack/spec/contracts';\nimport { emptyGroupValueFor, type FilterCondition } from '@objectstack/spec/data';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\nimport { filterTokenContextFrom, resolveFilterTokens } from '@objectstack/core';\nimport type { CompiledDataset, DerivedMeasureSpec } from './dataset-compiler.js';\nimport type { OrderLabelResolver } from './dimension-labels.js';\n\n// Re-export the shared protocol shapes so existing importers keep working.\nexport type { DatasetSelection } from '@objectstack/spec/contracts';\n/** @deprecated use DatasetCompareTo from @objectstack/spec/contracts */\nexport type CompareTo = DatasetCompareTo;\n\n/**\n * Dataset executor (ADR-0021 WS2).\n *\n * Turns a compiled dataset + a presentation's selection (dimensions, measures,\n * runtime filter, compareTo) into one or more `AnalyticsQuery`s against the Cube\n * runtime, then post-processes the results:\n * - resolves the base measures a selection needs (including derived deps),\n * - applies measure-scoped filters via supplementary grouped queries — in\n * EVERY window it runs, the `compareTo` one included (#4820),\n * - fills the empty-group value into columns no query reported, by aggregate\n * kind (#4708) — a count/sum over an excluded group is 0, avg/min/max null,\n * - evaluates derived measures (ratio/sum/difference/product) row-by-row (Q1),\n * - shifts the queries for `compareTo` (previousPeriod / previousYear) and\n * attaches `<measure>__compare` columns, re-running the same measure pass\n * so a filtered measure means the same thing in both columns,\n * - computes server-side totals (`selection.totals.groupings`, #1753) by\n * re-running the selection per dimension subset, so matrix subtotals and\n * the grand total use each measure's true aggregate,\n * - orders and windows the final grid (`order` / `limit` / `offset`, #3588).\n *\n * **Where ordering happens, and why here.** `order`/`limit`/`offset` are applied\n * to the ASSEMBLED grid — after measure-scoped sub-queries are merged in, after\n * `compareTo` columns are attached, and after derived measures are computed —\n * never by forwarding them blindly to every sub-query. Two reasons:\n *\n * 1. **Correctness.** A supplementary measure-scoped query selects ONE measure;\n * forwarding `ORDER BY <other_measure>` to it emits SQL referencing a column\n * that query never selects, and forwarding `LIMIT` truncates it before the\n * merge, so rows silently vanish from the grid. A derived measure has no SQL\n * column at all, yet is a perfectly reasonable sort key.\n * 2. **Coverage.** Only `NativeSQLStrategy` honours `order`/`limit`; the\n * ObjectQL aggregate path has nowhere to put them (`EngineAggregateOptions`\n * has no ordering grammar), and date-bucketed queries are *forced* down that\n * path because native SQL declines granularity. Sorting here makes ordering\n * work identically on every driver and strategy.\n *\n * The single-query case still pushes `order`/`limit`/`offset` DOWN into the SQL\n * (see `canPushDownWindow`) so the database does the work and the echoed `sql`\n * shows it; the post-pass is then a no-op re-sort of already-sorted rows.\n *\n * **What the sort key IS for a label-bearing dimension (#3680).** An order key\n * naming a `select` or `lookup`/`master_detail` dimension sorts by the DISPLAY\n * label the response will carry (option label / related record name), not the\n * stored value — a \"sort by Account\" ordered by opaque FK ids presents as\n * arbitrary once the labels render. The mapping comes through an injected\n * {@link OrderLabelResolver} (built by `queryDataset` over the same\n * label-resolution capabilities the display pass uses); rows keep their raw\n * values — only the COMPARISON substitutes the label — so drill metadata still\n * snapshots stored values downstream. Such keys are never pushed into SQL (the\n * label is not a column there), and the label fetch happens BEFORE `applyWindow`\n * so a \"top 10 by account name\" truncates the right ten.\n *\n * RLS/tenant scoping is NOT handled here — it is enforced inside the strategy\n * via the StrategyContext read-scope hook (D-C). This layer is pure query\n * shaping + arithmetic; the order-label hook is an injected interface, not an\n * engine dependency.\n */\n\n/**\n * Expand `{filter-placeholder}` values across everything a dataset query\n * compares on (framework#3582): the dataset's intrinsic `filter`, the\n * presentation's `runtimeFilter` (a dashboard widget's own scope), every\n * measure-scoped filter, and the `dateRange` bounds of the selection's time\n * dimensions.\n *\n * The dashboard path needs its own call rather than inheriting the ObjectQL\n * engine's: `NativeSQLStrategy` compiles a raw `SELECT … WHERE` and binds\n * comparands directly, so a widget filtered on `{current_year_start}` never\n * passes through `engine.find()` at all — which is exactly why the token\n * reached SQLite as the literal text and every such widget rendered zero.\n *\n * Inputs are treated as immutable: a `CompiledDataset` lives in the service's\n * registry across requests, so resolving in place would bake one request's\n * user id (and one day's dates) into every later render. New objects are\n * allocated only when the tree actually held a placeholder.\n */\nfunction resolveSelectionTokens(\n compiled: CompiledDataset,\n selection: DatasetSelection,\n context?: ExecutionContext,\n): { compiled: CompiledDataset; selection: DatasetSelection } {\n // One instant for the whole call: the intrinsic filter, the runtime filter\n // and each measure filter are resolved in separate passes, and a query whose\n // pieces disagreed about \"now\" could straddle a period boundary — the primary\n // grid scoped to this month while a measure-scoped sub-query saw the next.\n const tokenCtx = filterTokenContextFrom(context, new Date());\n const resolve = <T>(v: T): T => resolveFilterTokens(v, tokenCtx);\n\n const filter = resolve(compiled.filter);\n const measureFilters = resolve(compiled.measureFilters);\n const runtimeFilter = resolve(selection.runtimeFilter);\n const timeDimensions = selection.timeDimensions?.map((td) =>\n td.dateRange == null ? td : { ...td, dateRange: resolve(td.dateRange) },\n );\n\n const compiledChanged =\n filter !== compiled.filter || measureFilters !== compiled.measureFilters;\n const selectionChanged =\n runtimeFilter !== selection.runtimeFilter ||\n (timeDimensions !== undefined &&\n timeDimensions.some((td, i) => td !== selection.timeDimensions![i]));\n\n return {\n compiled: compiledChanged ? { ...compiled, filter, measureFilters } : compiled,\n selection: selectionChanged ? { ...selection, runtimeFilter, timeDimensions } : selection,\n };\n}\n\n/** AND two optional FilterConditions into one (MongoDB-style). */\nexport function combineFilters(\n a?: FilterCondition,\n b?: FilterCondition,\n): FilterCondition | undefined {\n if (a && b) return { $and: [a, b] } as FilterCondition;\n return a ?? b;\n}\n\n/**\n * Partition base measures into those the dataset scopes with their own\n * measure-level `filter` and those it does not — the single place that answers\n * \"does this measure carry its own filter?\".\n *\n * Paired with {@link DatasetExecutor.runMeasurePass}, this is what keeps ONE\n * definition of \"how a measure filter is applied\" for every grouped pass the\n * executor runs: the current period, each `totals` subset, and the `compareTo`\n * window. `compareTo` used to issue a single shifted query over all base\n * measures with only the base filter, consulting `measureFilters` nowhere on\n * that path — so a measure declared `filter: { stage: 'closed_won' }` was\n * scoped in its own column and unscoped in `<measure>__compare`: two different\n * measures rendered side by side under one label, and biased the worst way\n * (the comparison window is inflated by exactly the rows the measure exists to\n * exclude, so \"won deals vs. last month\" reads as a collapse). #4820.\n *\n * The remedy is deliberately NOT a second copy of the filter logic on the\n * compare path — two implementations of one rule diverge again at the next\n * change. Both paths call the same split and the same pass.\n */\nexport function splitMeasuresByFilter(\n measures: Iterable<string>,\n measureFilters: Record<string, FilterCondition | undefined>,\n): { unfiltered: string[]; filtered: string[] } {\n const unfiltered: string[] = [];\n const filtered: string[] = [];\n for (const m of measures) (measureFilters[m] ? filtered : unfiltered).push(m);\n return { unfiltered, filtered };\n}\n\n/**\n * Evaluate derived measures on each aggregated row, mutating a shallow copy.\n * Division by zero (and missing operands) yields `null` rather than Infinity/NaN.\n */\nexport function evaluateDerivedMeasures(\n rows: Record<string, unknown>[],\n derived: DerivedMeasureSpec[],\n): Record<string, unknown>[] {\n if (derived.length === 0) return rows;\n return rows.map((row) => {\n const out = { ...row };\n for (const d of derived) {\n out[d.name] = computeDerived(d, out);\n }\n return out;\n });\n}\n\n/**\n * Fill the EMPTY-GROUP value into every measure column the assembled grid\n * LISTS but no query REPORTED — by aggregate kind (#4708, objectui#3136).\n *\n * The grid is assembled from several results: the primary query, one\n * supplementary query per measure-scoped filter, and (for `compareTo`) a\n * shifted pass. {@link mergeByDimensions} writes a measure's column only onto\n * rows its source result returned, and a `GROUP BY` over a filtered row set\n * emits NO group at all for a dimension value the filter excludes entirely.\n * The column therefore comes back **absent**, not `0` — and absent renders as\n * \"no data for this row\", which for a count is the opposite of what the row\n * means. A derived ratio over it goes null as well ({@link computeDerived}\n * treats a missing operand as unknowable), so the blank spreads.\n *\n * The bias runs the worst possible way: the rows that blank are the ones whose\n * numerator the filter excluded — the WORST-performing rows. A `lead_source`\n * that won nothing renders as \"no data\" while one that won everything renders\n * fine.\n *\n * **Filled strictly by aggregate kind**, never wholesale. `count` /\n * `count_distinct` over an excluded group is unambiguously `0` (\"how many rows\n * matched\" has an exact answer when the answer is none), and `sum` over the\n * empty set is its identity `0`. `avg` / `min` / `max` are genuinely null —\n * there is nothing to average — and flattening those to `0` would trade this\n * lie for the opposite one, reporting a measurement nobody made. The\n * kind→identity mapping is `emptyGroupValueFor` in `@objectstack/spec/data`,\n * shared with the authoring-side coherence checks so the two cannot drift.\n *\n * **Only rows that already exist are touched** — no group is invented. A\n * dimension value no query reported at all has genuinely no data and stays out\n * of the grid; this fills the cell, never the row.\n *\n * Deliberately NOT a `?? 0` in the widget or a `coalesce` in the measure: a\n * consumer-side patch must be repeated by every author of every ratio widget\n * forever, and forgetting it is silent. Only the executor knows which aggregate\n * produced the gap, so only the executor can tell `0` from unknown.\n *\n * Mutates `rows` in place (they are already this pipeline's own copies) and\n * returns them for chaining.\n *\n * @param columnAggregates - Grid column → the aggregate that produced it.\n * Includes `<measure>__compare` columns, which merge through the same seam.\n */\nexport function fillEmptyGroups(\n rows: Record<string, unknown>[],\n columnAggregates: Record<string, string | undefined>,\n): Record<string, unknown>[] {\n for (const [column, aggregate] of Object.entries(columnAggregates)) {\n const empty = emptyGroupValueFor(aggregate);\n if (empty === undefined) continue;\n for (const row of rows) if (row[column] == null) row[column] = empty;\n }\n return rows;\n}\n\nfunction num(v: unknown): number | null {\n if (v == null) return null;\n const n = typeof v === 'number' ? v : Number(v);\n return Number.isFinite(n) ? n : null;\n}\n\nfunction computeDerived(d: DerivedMeasureSpec, row: Record<string, unknown>): number | null {\n const vals = d.of.map((name) => num(row[name]));\n if (vals.some((v) => v === null)) return null;\n const nums = vals as number[];\n switch (d.op) {\n case 'ratio': {\n if (nums.length < 2 || nums[1] === 0) return null;\n return nums[0] / nums[1];\n }\n case 'difference':\n return nums.slice(1).reduce((acc, v) => acc - v, nums[0]);\n case 'sum':\n return nums.reduce((acc, v) => acc + v, 0);\n case 'product':\n return nums.reduce((acc, v) => acc * v, 1);\n default:\n return null;\n }\n}\n\n// ── date bucketing (#3588) ───────────────────────────────────────────────────\n\n/** The date-bucket vocabulary shared by the dataset, the selection, and the\n * bucketing utilities in `@objectstack/core`. */\nexport type DateGranularityValue = NonNullable<DatasetSelection['dateGranularity']>;\n\n/**\n * The EFFECTIVE bucket size for one date dimension of a selection — the single\n * source of truth for granularity precedence.\n *\n * Precedence, per dimension:\n * 1. a `granularity` already stated on that dimension's `timeDimensions`\n * entry — never overridden;\n * 2. `selection.dateGranularity` — the presentation's choice, so a widget can\n * bucket by month without the dataset committing every consumer to it;\n * 3. `datasetDefault` — the dataset dimension's own `dateGranularity`.\n *\n * The unit of precedence is the GRANULARITY, not the entry: a `timeDimensions`\n * entry carrying only a `dateRange` (what `compareTo` needs) states a WINDOW,\n * not a bucket size, and must not suppress bucketing.\n *\n * **Why this is exported.** The bucket size chosen here decides three things\n * that MUST agree: the `GROUP BY` the query compiles to, the humanized label\n * each bucket key is rendered as, and the half-open `[gte, lt)` range a bucket\n * drills into. When the query layer resolved granularity and the post-processing\n * in `analytics-service` read the dataset default instead, they silently\n * disagreed for every selection that overrode it — a `year` query came back\n * labelled `1970-01` (a year bucket re-formatted as a month), a `day` query\n * collapsed to duplicate month labels, and `quarter`/`year` lost their drill\n * ranges entirely. One function, called from all three sites, is what stops\n * that drift recurring.\n */\nexport function resolveDimensionGranularity(\n selection: Pick<DatasetSelection, 'timeDimensions' | 'dateGranularity'>,\n dimension: string,\n datasetDefault?: string,\n): DateGranularityValue | undefined {\n // `timeDimensions[].granularity` and the compiled cube's `granularities` are\n // both typed as bare strings by their own layers (Cube.js heritage), but the\n // only values that reach here come from the dataset/selection granularity\n // vocabulary — the same five the bucketing utilities accept.\n const stated = (selection.timeDimensions ?? []).find((t) => t.dimension === dimension)?.granularity;\n if (stated) return stated as DateGranularityValue;\n return selection.dateGranularity ?? (datasetDefault as DateGranularityValue | undefined);\n}\n\n// ── ordering + windowing (#3588) ─────────────────────────────────────────────\n\n/**\n * Compare two grouped-cell values for ORDER BY, ascending.\n *\n * Nulls sort LAST regardless of direction (the SQL `NULLS LAST` convention, and\n * the one users expect: an empty bucket shouldn't win a \"top 10 by revenue\").\n * The caller negates the result for `desc`, so the null branch deliberately\n * returns its verdict BEFORE that negation can flip it — see `compareRows`.\n *\n * Numbers (and numeric strings, which is how some drivers return SUM results)\n * compare numerically so 9 sorts below 10; everything else compares as a string.\n * Dates arrive here already bucketed to sort-stable keys (\"2026-04\", \"2026-Q2\"),\n * so lexicographic ordering is chronological for them too.\n */\nfunction compareValues(a: unknown, b: unknown): number {\n const aNull = a == null || a === '';\n const bNull = b == null || b === '';\n if (aNull || bNull) return aNull && bNull ? 0 : aNull ? 1 : -1;\n if (a instanceof Date || b instanceof Date) {\n return Number(a instanceof Date ? a.getTime() : a) - Number(b instanceof Date ? b.getTime() : b);\n }\n if (typeof a === 'boolean' || typeof b === 'boolean') {\n return Number(a) - Number(b);\n }\n const an = typeof a === 'number' ? a : Number(a);\n const bn = typeof b === 'number' ? b : Number(b);\n if (Number.isFinite(an) && Number.isFinite(bn)) return an - bn;\n return String(a).localeCompare(String(b));\n}\n\n/**\n * Order rows by each key in `order`, in the object's own key order (first key is\n * the primary sort). Returns a NEW array; the input is not mutated. Null/empty\n * cells stay last in both directions (see {@link compareValues}).\n *\n * `sortKeys` substitutes the COMPARED value per key (#3680): when it holds a map\n * for an order key, each cell compares by its mapped value — the display label a\n * label-bearing dimension will render as — falling back to the raw cell where\n * unmapped (an orphaned id or RLS-hidden record renders raw too, so sort and\n * display stay consistent). The rows themselves are never rewritten here.\n */\nexport function applyOrdering(\n rows: Record<string, unknown>[],\n order: Record<string, 'asc' | 'desc'> | undefined,\n sortKeys?: Record<string, Map<unknown, unknown>>,\n): Record<string, unknown>[] {\n const keys = Object.entries(order ?? {});\n if (keys.length === 0 || rows.length < 2) return rows;\n // Array.prototype.sort is stable (ES2019+), so equal rows keep the order the\n // grouping produced — an important property for reproducible LIMITs.\n return [...rows].sort((ra, rb) => {\n for (const [key, dir] of keys) {\n const map = sortKeys?.[key];\n const av = map?.get(ra[key]) ?? ra[key];\n const bv = map?.get(rb[key]) ?? rb[key];\n const aNull = av == null || av === '';\n const bNull = bv == null || bv === '';\n // Nulls last in BOTH directions — decided before `desc` negation.\n if (aNull || bNull) {\n if (aNull && bNull) continue;\n return aNull ? 1 : -1;\n }\n const c = compareValues(av, bv);\n if (c !== 0) return dir === 'desc' ? -c : c;\n }\n return 0;\n });\n}\n\n/** Apply `offset`/`limit` to an already-ordered grid. */\nexport function applyWindow(\n rows: Record<string, unknown>[],\n limit?: number,\n offset?: number,\n): Record<string, unknown>[] {\n const start = offset != null && offset > 0 ? offset : 0;\n if (start === 0 && limit == null) return rows;\n return rows.slice(start, limit != null ? start + limit : undefined);\n}\n\n/**\n * Validate `order` keys and resolve the EFFECTIVE ordering for a selection.\n *\n * A key must name something the caller actually selected — a dimension, a\n * measure, or a `<measure>__compare` column. An unknown key throws rather than\n * being dropped: silently ignoring `sortBy` is precisely the failure mode this\n * change exists to remove (#3588), and a mistyped sort key that quietly returns\n * arbitrarily-ordered rows is worse than a loud 400.\n *\n * When `limit`/`offset` is requested WITHOUT an order, the selected dimensions\n * ascending become the implicit ordering, so the truncated window is\n * reproducible instead of \"whatever the group-by happened to emit\".\n *\n * Failing both, a selected TIME dimension defaults to ASCENDING (#3916). A time\n * axis has one order a reader expects — chronological — and until this default\n * existed nothing supplied it anywhere in the stack: the aggregate path has no\n * ordering grammar, so buckets came back in Map-insertion order, and the pivot\n * builds its column headers in row-arrival order. A month-bucketed matrix\n * therefore rendered `2026-07-01, 2026-07-05, …, 2026-07-02`. Bucket keys are\n * minted sort-stable for exactly this (`2026-07`, `2026-Q3`, `2026-W31`), so\n * ascending IS chronological. An explicit `order` still wins outright — this is\n * a default, not a policy — and non-time dimensions keep whatever order the\n * grouping produced unless the caller asks.\n *\n * @param timeDimensions - The selected dimensions the cube types as `time`, in\n * selection order. The executor resolves these (it owns the cube); passing\n * them in keeps this function pure and directly testable.\n */\nexport function resolveOrdering(\n selection: DatasetSelection,\n dimensions: string[],\n timeDimensions: string[] = [],\n): Record<string, 'asc' | 'desc'> | undefined {\n const order = selection.order;\n if (order && Object.keys(order).length > 0) {\n const selectable = new Set<string>([\n ...dimensions,\n ...selection.measures,\n ...selection.measures.map((m) => `${m}__compare`),\n ]);\n const unknown = Object.keys(order).filter((k) => !selectable.has(k));\n if (unknown.length) {\n throw new Error(\n `[dataset-executor] order key(s) ${unknown.map((k) => `\"${k}\"`).join(', ')} — ` +\n `not a selected dimension or measure. Selectable here: ` +\n `${[...selectable].join(', ') || '(none)'}.`,\n );\n }\n return order;\n }\n // Implicit, deterministic ordering so a bare `limit` is reproducible.\n if ((selection.limit != null || selection.offset != null) && dimensions.length > 0) {\n return Object.fromEntries(dimensions.map((d) => [d, 'asc' as const]));\n }\n // #3916 — chronological by default on the time axis.\n const timeKeys = timeDimensions.filter((d) => dimensions.includes(d));\n if (timeKeys.length > 0) {\n return Object.fromEntries(timeKeys.map((d) => [d, 'asc' as const]));\n }\n return undefined;\n}\n\n// ── compareTo date math (deterministic — no Date.now) ────────────────────────\n\nfunction parseUTC(date: string): number {\n // Accepts 'YYYY-MM-DD' (and ISO datetimes); interpreted as UTC.\n const ms = Date.parse(date.length === 10 ? `${date}T00:00:00Z` : date);\n if (Number.isNaN(ms)) throw new Error(`[dataset-executor] invalid date in dateRange: \"${date}\"`);\n return ms;\n}\n\nconst DAY_MS = 86_400_000;\n\nfunction toISODate(ms: number): string {\n return new Date(ms).toISOString().slice(0, 10);\n}\n\nfunction shiftYear(date: string, years: number): string {\n const d = new Date(parseUTC(date));\n d.setUTCFullYear(d.getUTCFullYear() + years);\n return toISODate(d.getTime());\n}\n\n/** Compute the comparison window for a [start,end] range. */\nexport function shiftRange(range: [string, string], kind: CompareTo['kind']): [string, string] {\n const [start, end] = range;\n if (kind === 'previousYear') {\n return [shiftYear(start, -1), shiftYear(end, -1)];\n }\n // previousPeriod — the equal-length window ending the day before `start`.\n const startMs = parseUTC(start);\n const endMs = parseUTC(end);\n const lengthDays = Math.round((endMs - startMs) / DAY_MS) + 1;\n const prevEndMs = startMs - DAY_MS;\n const prevStartMs = prevEndMs - (lengthDays - 1) * DAY_MS;\n return [toISODate(prevStartMs), toISODate(prevEndMs)];\n}\n\nexport class DatasetExecutor {\n /**\n * @param service - The analytics service the executor issues its queries to.\n * @param orderLabels - Optional sort-key label hook (#3680). When provided,\n * an order key naming a label-bearing (`select`/`lookup`) dimension sorts\n * by its display label instead of the stored value. Omit to sort by stored\n * values everywhere (e.g. the draft-preview path, whose seed rows already\n * carry display names).\n */\n constructor(\n private readonly service: IAnalyticsService,\n private readonly orderLabels?: OrderLabelResolver,\n ) {}\n\n /**\n * Execute a dataset selection and return the shaped rows (+ field metadata).\n *\n * @param context - The request's ExecutionContext, threaded into every\n * underlying `IAnalyticsService.query` so the tenant/RLS read scope is\n * applied per request (ADR-0021 D-C).\n */\n async execute(\n compiledInput: CompiledDataset,\n selectionInput: DatasetSelection,\n context?: ExecutionContext,\n ): Promise<AnalyticsResult> {\n // framework#3582 — expand `{current_quarter_start}` / `{current_user_id}`\n // placeholders BEFORE any query is shaped, once for the whole call so every\n // sub-query (measure-scoped, totals, compareTo) shares one instant.\n const { compiled, selection } = resolveSelectionTokens(compiledInput, selectionInput, context);\n\n const result = await this.executeSelection(compiled, selection, context);\n\n // Server-side totals (#1753) — re-run the selection grouped by each\n // requested dimension subset, so a subtotal/grand total is the measure's\n // TRUE aggregate over the underlying rows (an avg total is the average of\n // all rows, not of bucket averages). Re-running the full pipeline keeps\n // measure-scoped filters, derived measures, and compareTo consistent with\n // the primary grid. order/limit/offset are dropped: totals cover the whole\n // selection, and an order key may reference a dimension the grouping drops.\n const groupings = selection.totals?.groupings;\n if (groupings?.length) {\n const selected = new Set(selection.dimensions ?? []);\n const totals: NonNullable<AnalyticsResult['totals']> = [];\n for (const grouping of groupings) {\n const unknown = grouping.filter((d) => !selected.has(d));\n if (unknown.length) {\n throw new Error(\n `[dataset-executor] totals grouping [${grouping.join(', ')}] is not a subset of the selected dimensions — unknown: ${unknown.join(', ')}.`,\n );\n }\n const sub = await this.executeSelection(compiled, {\n ...selection,\n dimensions: grouping,\n totals: undefined,\n order: undefined,\n limit: undefined,\n offset: undefined,\n }, context);\n totals.push({ dimensions: grouping, rows: sub.rows });\n }\n result.totals = totals;\n }\n\n return result;\n }\n\n private async executeSelection(\n compiled: CompiledDataset,\n selection: DatasetSelection,\n context?: ExecutionContext,\n ): Promise<AnalyticsResult> {\n const derivedByName = new Map(compiled.derived.map((d) => [d.name, d]));\n const selectedDerived = selection.measures\n .map((m) => derivedByName.get(m))\n .filter((d): d is DerivedMeasureSpec => !!d);\n\n // Base measures = selected non-derived + dependencies of selected derived.\n const baseMeasures = new Set<string>();\n for (const m of selection.measures) {\n if (!derivedByName.has(m)) baseMeasures.add(m);\n }\n for (const d of selectedDerived) {\n for (const dep of d.of) baseMeasures.add(dep);\n }\n\n // Split measures into those with a scoped filter and those without.\n const { unfiltered, filtered } = splitMeasuresByFilter(baseMeasures, compiled.measureFilters);\n\n const baseFilter = combineFilters(compiled.filter, selection.runtimeFilter);\n const dimensions = selection.dimensions ?? [];\n\n // Effective ordering — validated against what this selection projects, with\n // a deterministic dimension order synthesized for a bare `limit` (#3588) and\n // an ascending default on the time axis (#3916).\n const order = resolveOrdering(selection, dimensions, this.timeDimensionsOf(compiled, dimensions));\n\n // #3680 — order keys naming a select/lookup dimension sort by the DISPLAY\n // label the response will carry, not the stored value / FK id. Resolved\n // over the assembled grid below; identified up front because such a key\n // also disqualifies SQL pushdown (the label is not a column the database\n // could ORDER BY, and a SQL LIMIT would truncate the wrong window).\n const labelOrderKeys = this.orderLabels\n ? Object.keys(order ?? {}).filter(\n (k) => dimensions.includes(k) && this.orderLabels!.isLabelBearing(k),\n )\n : [];\n\n // Push `order`/`limit`/`offset` down into the SQL only when this selection\n // is ONE query whose columns can satisfy them. With supplementary\n // measure-scoped queries, a compareTo pass, or derived measures in play, the\n // grid is assembled from several results — a sub-query LIMIT would drop rows\n // before the merge and an ORDER BY would name a column that sub-query never\n // selects. Those cases order in memory below instead.\n const singleQuery = filtered.length === 0 && !selection.compareTo && selectedDerived.length === 0;\n const pushDownKeys = new Set<string>([...dimensions, ...unfiltered]);\n const canPushDownWindow =\n singleQuery && labelOrderKeys.length === 0 &&\n Object.keys(order ?? {}).every((k) => pushDownKeys.has(k));\n const windowQuery = canPushDownWindow\n ? { order, limit: selection.limit, offset: selection.offset }\n : undefined;\n\n // The current-period pass: unfiltered base measures in one query plus one\n // supplementary query per measure-scoped filter, merged by dimension key.\n const result = await this.runMeasurePass(compiled, selection, {\n measures: [...baseMeasures],\n dimensions,\n baseFilter,\n window: windowQuery,\n context,\n });\n\n // compareTo — run the SAME pass over the shifted window and attach.\n if (selection.compareTo) {\n const compareRows = await this.runCompare(compiled, selection, [...baseMeasures], dimensions, baseFilter, context);\n result.rows = mergeByDimensions(\n result.rows,\n compareRows,\n dimensions,\n [...baseMeasures].map((m) => `${m}__compare`),\n );\n for (const m of baseMeasures) result.fields.push({ name: `${m}__compare`, type: 'number' });\n }\n\n // Empty-group fill (#4708) — a group a query never reported reads `0` for a\n // count/sum and stays null for avg/min/max. See {@link fillEmptyGroups} for\n // why this belongs to the executor and not to every widget author.\n //\n // Placed after EVERY merge and before the derived pass, because each merge\n // is a place a column can go missing and `mergeByDimensions` APPENDS rows:\n // - a supplementary measure-scoped query omits the groups its filter\n // excluded (the \"won nothing\" rows — the original defect);\n // - a later supplementary query can append rows for dimension keys no\n // earlier query saw, and those need the same fill;\n // - the compareTo pass appends a row for every bucket that existed in the\n // PREVIOUS window and not in this one, on which *every* base measure is\n // absent — including unfiltered ones, which is why the fill covers all\n // base measures rather than only the filter-scoped ones.\n // Running it before the compare merge left that last class blank, so a lead\n // source that sold last month and nothing this month rendered as \"no data\"\n // instead of 0 — the same worst-row bias, one merge later.\n //\n // Derived measures are evaluated AFTER, so a ratio over a filled 0 computes\n // (0%) instead of being poisoned by an absent operand.\n const fillColumns: Record<string, string | undefined> = {};\n for (const m of baseMeasures) {\n const aggregate = compiled.cube.measures?.[m]?.type;\n fillColumns[m] = aggregate;\n if (selection.compareTo) fillColumns[`${m}__compare`] = aggregate;\n }\n fillEmptyGroups(result.rows, fillColumns);\n\n // Derived measures (computed from base + compare columns already present).\n result.rows = evaluateDerivedMeasures(result.rows, selectedDerived);\n for (const d of selectedDerived) result.fields.push({ name: d.name, type: 'number' });\n\n // Order + window the assembled grid (#3588). Every column the caller may\n // sort by exists by now — merged measure-scoped values, `__compare`\n // columns, and derived measures included. When the window was already\n // pushed into SQL this re-sorts an already-sorted grid (a no-op) and\n // re-slices an already-sliced one; when it could not be (the ObjectQL\n // aggregate path has no ordering grammar, and date-bucketed queries are\n // forced down it), this is what makes `sortBy` work at all.\n //\n // #3680 — for label-bearing order keys, substitute the display label as\n // the SORT KEY, resolved over the grid's distinct values BEFORE the window\n // (a \"top 10 by account name\" must pick the ten by name). Rows keep their\n // raw values — display rewriting stays in `queryDataset`, after the drill\n // metadata snapshots the stored values. A select dimension resolves from\n // field metadata (no query); a lookup costs one batched id→name read.\n let sortKeys: Record<string, Map<unknown, unknown>> | undefined;\n for (const key of labelOrderKeys) {\n const values = [...new Set(result.rows.map((r) => r[key]).filter((v) => v != null))];\n if (values.length === 0) continue;\n const labels = await this.orderLabels!.resolveLabels(key, values);\n if (labels && labels.size > 0) (sortKeys ??= {})[key] = labels;\n }\n result.rows = applyOrdering(result.rows, order, sortKeys);\n result.rows = applyWindow(result.rows, selection.limit, selection.offset);\n\n return result;\n }\n\n /**\n * Run ONE grouped pass over a set of base measures, honouring each measure's\n * own scoped `filter`: the unfiltered measures in a single query, plus one\n * supplementary query per filter-scoped measure, merged back by dimension key.\n *\n * **This is the executor's only implementation of \"how a measure filter is\n * applied\", and every window goes through it** — the current period, each\n * `totals` subset (which re-enters via `executeSelection`), and the\n * `compareTo` window. Before #4820 the comparison window had its own,\n * simpler answer: one shifted query over all base measures with only the\n * base filter, so `compiled.measureFilters` was never read on that path.\n * `won_count` counted won deals and `won_count__compare` counted every deal,\n * under one label, in adjacent columns. Only measures carrying a filter were\n * wrong — which is what made it survive: the unfiltered ones next to them\n * compared correctly.\n *\n * The caller supplies the `selection` this pass queries under, which is how\n * the comparison window differs at all: same measures, same dimensions, same\n * filters — a `timeDimensions` shifted by {@link shiftRange}. Nothing else\n * about the two passes may drift, because anything that does becomes a\n * discrepancy between two columns the reader is invited to subtract.\n *\n * Cost: one extra query per filter-scoped measure when `compareTo` is set.\n * The alternative — declaring the discrepancy in the response — is not one,\n * since the two columns exist to be directly comparable.\n *\n * @param window - Ordering/window to push into the SQL. Only ever set for a\n * selection the caller proved is a single self-sufficient query; a pass\n * that fans out must return its whole grid for the merge.\n */\n private async runMeasurePass(\n compiled: CompiledDataset,\n selection: DatasetSelection,\n opts: {\n measures: string[];\n dimensions: string[];\n baseFilter?: FilterCondition;\n window?: { order?: Record<string, 'asc' | 'desc'>; limit?: number; offset?: number };\n context?: ExecutionContext;\n },\n ): Promise<AnalyticsResult> {\n const { measures, dimensions, baseFilter, window, context } = opts;\n const { unfiltered, filtered } = splitMeasuresByFilter(measures, compiled.measureFilters);\n\n // Primary query: all unfiltered base measures in one pass. When every base\n // measure is filter-scoped, the supplementary queries below build the grid.\n let result: AnalyticsResult;\n if (unfiltered.length > 0 || filtered.length === 0) {\n result = await this.service.query(this.buildQuery(compiled, {\n measures: unfiltered,\n dimensions,\n where: baseFilter,\n selection,\n contextTimezone: context?.timezone,\n window,\n }), context);\n } else {\n result = { rows: [], fields: [] };\n }\n\n // Supplementary queries: one per measure-scoped filter, merged by dimension key.\n for (const m of filtered) {\n const mFilter = combineFilters(baseFilter, compiled.measureFilters[m]);\n const sub = await this.service.query(this.buildQuery(compiled, {\n measures: [m], dimensions, where: mFilter, selection,\n contextTimezone: context?.timezone,\n }), context);\n result.rows = mergeByDimensions(result.rows, sub.rows, dimensions, [m]);\n result.fields.push({ name: m, type: 'number' });\n }\n\n return result;\n }\n\n /**\n * The selected dimensions the compiled cube types as `time`, in selection\n * order (#3916) — the axis {@link resolveOrdering} defaults to ascending.\n *\n * Membership is decided by the DIMENSION's declared type, not by whether the\n * selection happens to bucket it: a `date` dimension left ungranulated groups\n * raw timestamps, and those want chronological order every bit as much as\n * month buckets do. (Both sort correctly — `compareValues` compares Dates and\n * ISO strings chronologically, and bucket keys are minted sort-stable.)\n */\n private timeDimensionsOf(compiled: CompiledDataset, dimensions: string[]): string[] {\n return dimensions.filter((d) => compiled.cube.dimensions[d]?.type === 'time');\n }\n\n private buildQuery(\n compiled: CompiledDataset,\n opts: {\n measures: string[];\n dimensions: string[];\n where?: FilterCondition;\n selection: DatasetSelection;\n contextTimezone?: string;\n /**\n * Ordering/window to push DOWN into this query. Set only for a selection\n * the caller proved is a single self-sufficient query (see\n * `canPushDownWindow`); omitted for supplementary/compare sub-queries,\n * which must return their full grid for the merge.\n */\n window?: { order?: Record<string, 'asc' | 'desc'>; limit?: number; offset?: number };\n },\n ): AnalyticsQuery {\n const q: AnalyticsQuery = {\n cube: compiled.cube.name,\n measures: opts.measures,\n dimensions: opts.dimensions,\n // Precedence: explicit selection tz → request's reference tz\n // (ExecutionContext.timezone, ADR-0053 Phase 2) → UTC.\n timezone: opts.selection.timezone ?? opts.contextTimezone ?? 'UTC',\n };\n if (opts.where) q.where = opts.where as Record<string, unknown>;\n // Bucket selected date dimensions. Without this a date dimension groups by\n // the raw timestamp — one bucket per ROW, which is why a \"new accounts by\n // month\" bar chart drew one bar per account instead of one per month\n // (#3588).\n //\n // Granularity precedence, per dimension:\n // 1. a `granularity` already stated on that dimension's\n // `selection.timeDimensions` entry — never overridden;\n // 2. `selection.dateGranularity` — the PRESENTATION's choice, so a widget\n // can bucket by month without the dataset committing every consumer to\n // that granularity;\n // 3. the dataset dimension's own default (the compiler lowers an explicit\n // `dateGranularity` to a single-entry `granularities`; the 5-entry\n // \"all granularities\" list means the dataset stated no default).\n //\n // Note the unit of precedence is the GRANULARITY, not the entry. A\n // `timeDimensions` entry that only carries a `dateRange` (which is exactly\n // what `compareTo` needs) states a WINDOW, not a bucket size — letting its\n // mere presence suppress bucketing left the compared pass grouping raw\n // timestamps while the primary pass grouped months, so the two grids shared\n // no dimension key and every `__compare` column came back empty.\n const selTimeDims = opts.selection.timeDimensions ?? [];\n const selDims = new Set(selTimeDims.map((t) => t.dimension));\n const granularityFor = (name: string): string | undefined => {\n const cd = compiled.cube.dimensions[name];\n if (cd?.type !== 'time') return undefined;\n const datasetDefault = cd.granularities?.length === 1 ? String(cd.granularities[0]) : undefined;\n return resolveDimensionGranularity(opts.selection, name, datasetDefault);\n };\n // Fill in a bucket size for caller-supplied entries that named none.\n const resolvedTimeDims = selTimeDims.map((t) => {\n if (t.granularity) return t;\n const granularity = granularityFor(t.dimension);\n return granularity ? { ...t, granularity } : t;\n });\n const explicitTimeDims: Array<{ dimension: string; granularity: string }> = [];\n for (const name of opts.dimensions) {\n if (selDims.has(name)) continue;\n const granularity = granularityFor(name);\n if (granularity) explicitTimeDims.push({ dimension: name, granularity });\n }\n const mergedTimeDims = [...resolvedTimeDims, ...explicitTimeDims];\n if (mergedTimeDims.length > 0) q.timeDimensions = mergedTimeDims as AnalyticsQuery['timeDimensions'];\n // Ordering/window: pushed down ONLY when the caller vouched for it. The\n // executor always re-applies both over the assembled grid, so omitting them\n // here costs correctness nothing — it only moves the work to memory.\n if (opts.window?.order && Object.keys(opts.window.order).length > 0) q.order = opts.window.order;\n if (opts.window?.limit != null) q.limit = opts.window.limit;\n if (opts.window?.offset != null) q.offset = opts.window.offset;\n return q;\n }\n\n private async runCompare(\n compiled: CompiledDataset,\n selection: DatasetSelection,\n measures: string[],\n dimensions: string[],\n baseFilter: FilterCondition | undefined,\n context?: ExecutionContext,\n ): Promise<Record<string, unknown>[]> {\n const cmp = selection.compareTo!;\n const td = (selection.timeDimensions ?? []).find((t) => t.dimension === cmp.dimension);\n if (!td || !td.dateRange) {\n throw new Error(\n `[dataset-executor] compareTo requires a timeDimension \"${cmp.dimension}\" with a dateRange.`,\n );\n }\n const range: [string, string] = Array.isArray(td.dateRange)\n ? [td.dateRange[0], td.dateRange[1] ?? td.dateRange[0]]\n : [td.dateRange, td.dateRange];\n const shifted = shiftRange(range, cmp.kind);\n const shiftedTd = (selection.timeDimensions ?? []).map((t) =>\n t.dimension === cmp.dimension ? { ...t, dateRange: shifted } : t,\n );\n // Run the SAME pass the current period ran, over the shifted window: same\n // measures, same dimensions, same base filter, and — since #4820 — the same\n // measure-scoped filters, applied by the same supplementary sub-queries.\n // Issuing one flat query here instead is what made `<measure>__compare`\n // report a different measure than the column beside it.\n //\n // Going through `runMeasurePass` (and so `buildQuery`) also keeps the\n // comparison pass bucketing its date dimensions EXACTLY like the primary\n // pass. Hand-rolling the query here skipped granularity resolution, so a\n // bucketed primary grid (\"2026-04\") was merged against raw-timestamp\n // comparison rows and no dimension key ever matched — every `__compare`\n // column came back empty. The shifted `timeDimensions` still win for their\n // own dimension (rule 1 of the precedence chain); `window` is deliberately\n // omitted — the comparison grid must stay whole for the merge.\n const sub = await this.runMeasurePass(\n compiled,\n { ...selection, timeDimensions: shiftedTd },\n { measures, dimensions, baseFilter, context },\n );\n // Rename measure columns to `<measure>__compare` so they merge alongside primary.\n return sub.rows.map((row) => {\n const out: Record<string, unknown> = {};\n for (const dim of dimensions) out[dim] = row[dim];\n for (const m of measures) out[`${m}__compare`] = row[m];\n return out;\n });\n }\n}\n\n/**\n * Left-merge `extra` rows onto `base` rows by their dimension-key tuple,\n * copying the listed value columns. Rows in `extra` with no base match are\n * appended (outer-ish merge so comparison-only buckets still surface).\n */\nexport function mergeByDimensions(\n base: Record<string, unknown>[],\n extra: Record<string, unknown>[],\n dimensions: string[],\n valueColumns: string[],\n): Record<string, unknown>[] {\n const keyOf = (row: Record<string, unknown>) => dimensions.map((d) => String(row[d] ?? '')).join('\u0001');\n const index = new Map<string, Record<string, unknown>>();\n for (const row of base) index.set(keyOf(row), row);\n\n for (const row of extra) {\n const key = keyOf(row);\n const target = index.get(key);\n if (target) {\n for (const c of valueColumns) target[c] = row[c];\n } else {\n const fresh: Record<string, unknown> = {};\n for (const d of dimensions) fresh[d] = row[d];\n for (const c of valueColumns) fresh[c] = row[c];\n index.set(key, fresh);\n base.push(fresh);\n }\n }\n return base;\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Dimension display-label resolution (ADR-0021).\n *\n * Analytics groups by the raw stored value of a dimension field. For two field\n * kinds that value is NOT human-readable:\n *\n * - **select** — grouped by the stored option `value` (e.g. `backlog`), but the\n * user-facing text is the option `label` (e.g. `Backlog`).\n * - **lookup / master_detail** — grouped by the foreign-key `id` (e.g.\n * `8eqtuKI4G9IhUsPS`), but the user-facing text is the related record's\n * display field (its name/title).\n *\n * `resolveDimensionLabels` post-processes the result rows IN PLACE, replacing the\n * raw value at `row[dimension.name]` with its display label when one is found.\n * Unresolved values are left untouched so an orphaned id still renders as itself\n * rather than blanking out. Date / number / plain-string dimensions are no-ops.\n *\n * The resolution LOGIC lives here (and is unit-tested); the low-level capabilities\n * — reading an object's field map and fetching id→label pairs — are injected via\n * {@link DimensionLabelDeps} so this module stays free of any engine dependency.\n */\n\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\n\n/** The minimal field shape this resolver needs. */\nexport interface FieldMetaLite {\n type?: string;\n /** Lookup / master_detail target object name. */\n reference?: string;\n /** Select options — the value→label source. */\n options?: Array<{ value: unknown; label?: string }>;\n}\n\n/** Capabilities the resolver needs from the runtime (injected by the plugin). */\nexport interface DimensionLabelDeps {\n /** Return the field map for an object, or `undefined` if unknown. */\n getObjectFields(objectName: string): Record<string, FieldMetaLite> | undefined;\n /**\n * Fetch a map of `id → display label` for the given ids of a target object.\n * The implementation chooses the target's display field. Returning an empty\n * map (e.g. no display field, no data access) leaves the ids unresolved.\n *\n * `scope` (ADR-0021 D-C, #3602) is the TARGET object's own read scope — the\n * RLS/tenant `FilterCondition` the implementation must AND into the label\n * lookup so this never reveals a related record the target object's RLS would\n * hide. The label lookup is a per-record read (`group by id`) dressed as an\n * aggregate; without the scope it leaks display names whenever the referenced\n * object is more restricted than the base object whose rows carry the id.\n * `undefined` means \"no scope for this object\" (global table / unrestricted\n * caller) — the same contract as the read-scope provider.\n *\n * `context` is the request's ExecutionContext — the SECOND belt on the same\n * read (#3602). `scope` is the analytics layer's own predicate; forwarding the\n * context lets the ENGINE's middleware chain scope this per-record read\n * itself, so it stays scoped even if a caller ever reaches this hook without\n * a resolved `scope`. Implementations bridging to an ObjectQL engine MUST\n * forward it; a bridge with nowhere to put it may ignore it.\n */\n fetchRecordLabels(\n targetObject: string,\n ids: unknown[],\n scope?: Record<string, unknown>,\n context?: ExecutionContext,\n ): Promise<Map<unknown, string>>;\n}\n\n/**\n * Resolve the TARGET object's read scope for a label lookup (#3602). Returns the\n * object's RLS/tenant `FilterCondition`, `null`/`undefined` when the object is\n * unscoped, or a rejected promise when the scope cannot be resolved — in which\n * case the resolver fails CLOSED (skips that dimension's labels) rather than\n * fetching unscoped names.\n */\nexport type LabelScopeResolver = (\n targetObject: string,\n) => Promise<Record<string, unknown> | null | undefined> | Record<string, unknown> | null | undefined;\n\nconst LOOKUP_TYPES = new Set(['lookup', 'master_detail']);\n\n/**\n * Sort-key label resolution for `DatasetSelection.order` (#3680).\n *\n * The executor sorts the assembled grid BEFORE `queryDataset` rewrites stored\n * dimension values into display labels, so an order key naming a `select` or\n * `lookup`/`master_detail` dimension used to sort by the stored value / FK id —\n * an order that presents as arbitrary once the labels render. This hook hands\n * the executor JUST the value→label mapping for such a dimension so it can sort\n * by what the user will actually read, while the rows keep their raw values\n * (drill metadata depends on them) and ordering + windowing stay one adjacent\n * step. The executor stays engine-free: it sees this interface, never the\n * engine behind it.\n */\nexport interface OrderLabelResolver {\n /**\n * Whether the dimension's stored value differs from the label it renders as\n * (`select` options, `lookup`/`master_detail` FK ids). Synchronous — the\n * executor consults it when deciding whether the window may be pushed into\n * SQL, before any query runs.\n */\n isLabelBearing(dimension: string): boolean;\n /**\n * Map the given raw stored values of one dimension to display labels.\n * Values missing from the map sort by their raw form — the same thing the\n * user will see rendered for them.\n */\n resolveLabels(dimension: string, values: unknown[]): Promise<Map<unknown, string> | undefined>;\n}\n\n/**\n * Build the executor's {@link OrderLabelResolver} from the dataset's dimension\n * list and the injected label capabilities. Mirrors the classification in\n * {@link resolveDimensionLabels}: a dimension is label-bearing when its field\n * carries select `options` or is a lookup/master_detail with a `reference`.\n *\n * - `select` resolves from field metadata — no query at all.\n * - `lookup`/`master_detail` costs ONE batched id→name read over the distinct\n * grouped values, scoped to the REFERENCED object's own RLS (#3602). Fail\n * closed: an unresolvable scope degrades to sorting by the stored id rather\n * than fetching unscoped — consistent with the display pass, which renders\n * the raw id in that case too.\n */\nexport function createOrderLabelResolver(\n baseObject: string,\n dims: Array<{ name: string; field: string }>,\n deps: DimensionLabelDeps,\n resolveScope?: LabelScopeResolver,\n context?: ExecutionContext,\n): OrderLabelResolver {\n const dimByName = new Map(dims.map((d) => [d.name, d]));\n const metaFor = (dimension: string): FieldMetaLite | undefined => {\n const dim = dimByName.get(dimension);\n return dim ? deps.getObjectFields(baseObject)?.[dim.field] : undefined;\n };\n return {\n isLabelBearing(dimension) {\n const meta = metaFor(dimension);\n if (!meta) return false;\n if (Array.isArray(meta.options) && meta.options.length > 0) return true;\n return !!(meta.type && LOOKUP_TYPES.has(meta.type) && meta.reference);\n },\n async resolveLabels(dimension, values) {\n const meta = metaFor(dimension);\n if (!meta) return undefined;\n if (Array.isArray(meta.options) && meta.options.length > 0) {\n const labelByValue = new Map<unknown, string>();\n for (const opt of meta.options) {\n if (opt && opt.label != null) labelByValue.set(opt.value, String(opt.label));\n }\n return labelByValue;\n }\n if (meta.type && LOOKUP_TYPES.has(meta.type) && meta.reference) {\n let scope: Record<string, unknown> | null | undefined;\n if (resolveScope) {\n try {\n scope = await resolveScope(meta.reference);\n } catch {\n return undefined;\n }\n }\n return deps.fetchRecordLabels(meta.reference, values, scope ?? undefined, context);\n }\n return undefined;\n },\n };\n}\n\n/**\n * Wrap a {@link DimensionLabelDeps} so repeated `fetchRecordLabels` calls\n * within ONE request fetch each id at most once. A selection that sorts by a\n * lookup dimension resolves labels twice — once PRE-window for the sort keys\n * (#3680, over the full grid's ids), once post-window for display (a subset of\n * the same ids) — so with this cache the display pass costs no extra query.\n *\n * Per-request only: entries are keyed by target object alone, which is safe\n * because an object's read scope is constant within one request. Never share\n * an instance across requests.\n */\nexport function withLabelFetchCache(deps: DimensionLabelDeps): DimensionLabelDeps {\n // Per target object: id → label, with `null` marking \"fetched, no label\"\n // (RLS-hidden or orphaned) so unresolvable ids are not re-fetched every call.\n const cache = new Map<string, Map<unknown, string | null>>();\n return {\n getObjectFields: (objectName) => deps.getObjectFields(objectName),\n async fetchRecordLabels(targetObject, ids, scope, context) {\n let known = cache.get(targetObject);\n if (!known) {\n known = new Map();\n cache.set(targetObject, known);\n }\n const missing = ids.filter((id) => !known.has(id));\n if (missing.length > 0) {\n const fetched = await deps.fetchRecordLabels(targetObject, missing, scope, context);\n for (const id of missing) known.set(id, fetched.get(id) ?? null);\n }\n const out = new Map<unknown, string>();\n for (const id of ids) {\n const label = known.get(id);\n if (label != null) out.set(id, label);\n }\n return out;\n },\n };\n}\n\n/** Date-dimension granularity (mirrors the dataset `dateGranularity` enum). */\nexport type DateGranularity = 'day' | 'week' | 'month' | 'quarter' | 'year';\n\nconst pad = (n: number) => String(n).padStart(2, '0');\n\n/**\n * Format a raw date value (epoch-ms number, numeric string, ISO string, or\n * Date) to a human, sort-stable bucket label per granularity. Returns the input\n * unchanged when it isn't a parseable date, so a non-date value never blanks.\n *\n * year → \"2026\"\n * quarter → \"2026-Q2\"\n * month → \"2026-04\"\n * week → \"2026-04-13\" (ISO date of the bucket)\n * day → \"2026-04-15\"\n *\n * Intentionally UTC-only (ADR-0053 Phase 2): timezone bucketing happens\n * upstream in `bucketDate` / `bucketDateValue`, so by the time a value reaches\n * here it is *already* the reference-zone bucket (often a label string like\n * \"2026-Q2\"). Re-applying a timezone here would shift an already-correct\n * `YYYY-MM-DD` day bucket by a day — this is a pure, idempotent re-labeler.\n */\nexport function formatDateBucket(value: unknown, granularity?: DateGranularity | string): unknown {\n if (value == null || value instanceof Date === false) {\n if (typeof value !== 'number' && typeof value !== 'string') return value;\n }\n // A YEAR bucket's canonical key IS the bare year (\"2026\" / 2026) — which the\n // epoch heuristic below would read as 2026 milliseconds and relabel \"1970\".\n // Being idempotent over already-formatted bucket keys is this function's whole\n // contract, and every other granularity's key already survives the round trip\n // (\"2026-Q2\", \"2026-07\", \"2026-07-15\" all fail the pure-digit test); only the\n // year key collides with it. Recognised before parsing, for both the string\n // and numeric forms drivers return.\n if (granularity === 'year') {\n const y = typeof value === 'number' ? value : Number(String(value).trim());\n if (Number.isInteger(y) && y >= 1000 && y <= 9999) return String(y);\n }\n let d: Date;\n if (value instanceof Date) d = value;\n else if (typeof value === 'number') d = new Date(value);\n else {\n const s = String(value).trim();\n // Pure-digit strings are epoch millis (or seconds); otherwise let Date parse ISO.\n d = /^\\d+$/.test(s) ? new Date(Number(s) < 1e12 ? Number(s) * 1000 : Number(s)) : new Date(s);\n }\n if (Number.isNaN(d.getTime())) return value;\n const y = d.getUTCFullYear();\n const m = d.getUTCMonth(); // 0-11\n switch (granularity) {\n case 'year': return String(y);\n case 'quarter': return `${y}-Q${Math.floor(m / 3) + 1}`;\n case 'month': return `${y}-${pad(m + 1)}`;\n case 'week':\n case 'day':\n default: return `${y}-${pad(m + 1)}-${pad(d.getUTCDate())}`;\n }\n}\n\n/**\n * Replace raw dimension values with display labels, in place.\n *\n * @param baseObject - the dataset's base object (where the dimension fields live)\n * @param dims - selected dimensions as `{ name, field, type?, dateGranularity? }`\n * (row key = `name`)\n * @param rows - result rows, mutated in place\n * @param deps - injected runtime capabilities\n * @param resolveScope - (ADR-0021 D-C, #3602) resolves the referenced object's\n * own read scope for a lookup/master_detail dimension's label fetch. When it\n * throws, that dimension's labels are SKIPPED (fail-closed — the raw id renders\n * instead) rather than fetched unscoped. Omit when no read-scope provider is\n * configured (labels then fetch unscoped, as before — no security in play).\n * @param context - the request's ExecutionContext, forwarded to\n * {@link DimensionLabelDeps.fetchRecordLabels} so the engine's own middleware\n * scopes the per-record label read too — the second belt beside `resolveScope`\n * (#3602)\n */\nexport async function resolveDimensionLabels(\n baseObject: string,\n dims: Array<{ name: string; field: string; type?: string; dateGranularity?: DateGranularity | string }>,\n rows: Record<string, unknown>[],\n deps: DimensionLabelDeps,\n resolveScope?: LabelScopeResolver,\n context?: ExecutionContext,\n): Promise<void> {\n if (!rows.length || !dims.length) return;\n const fields = deps.getObjectFields(baseObject);\n if (!fields) return;\n\n for (const dim of dims) {\n const meta = fields[dim.field];\n\n // ── date: epoch / ISO → human bucket label ────────────────────────\n // A date dimension's grouped value is a raw timestamp (or a bucket start);\n // either way it must render as a readable date, not epoch millis.\n if (dim.type === 'date' || (meta && meta.type === 'date')) {\n for (const row of rows) {\n const formatted = formatDateBucket(row[dim.name], dim.dateGranularity);\n if (formatted != null) row[dim.name] = formatted;\n }\n continue;\n }\n\n if (!meta) continue;\n\n // ── select: value → option label ──────────────────────────────────\n if (Array.isArray(meta.options) && meta.options.length > 0) {\n const labelByValue = new Map<unknown, string>();\n for (const opt of meta.options) {\n if (opt && opt.label != null) labelByValue.set(opt.value, String(opt.label));\n }\n if (labelByValue.size === 0) continue;\n for (const row of rows) {\n const raw = row[dim.name];\n const label = labelByValue.get(raw);\n if (label != null) row[dim.name] = label;\n }\n continue;\n }\n\n // ── lookup / master_detail: id → related record display name ───────\n if (meta.type && LOOKUP_TYPES.has(meta.type) && meta.reference) {\n const ids = Array.from(\n new Set(rows.map((r) => r[dim.name]).filter((v) => v != null)),\n );\n if (ids.length === 0) continue;\n // #3602 — the label lookup reads the REFERENCED object by id. Scope it to\n // that object's own RLS so it never surfaces a related record the target's\n // RLS would hide (leak fires when the referenced object is stricter than\n // the base). Fail closed: if the scope can't be resolved, skip this\n // dimension's labels (raw id renders) rather than fetch unscoped.\n let scope: Record<string, unknown> | null | undefined;\n if (resolveScope) {\n try {\n scope = await resolveScope(meta.reference);\n } catch {\n continue;\n }\n }\n const labelById = await deps.fetchRecordLabels(meta.reference, ids, scope ?? undefined, context);\n if (!labelById || labelById.size === 0) continue;\n for (const row of rows) {\n const label = labelById.get(row[dim.name]);\n if (label != null) row[dim.name] = label;\n }\n }\n }\n}\n\n/**\n * Pick the display field for an object from its field map, by convention:\n * an explicit `name`/`title`/`label` field, else the first text-like field.\n * Returns `undefined` when nothing suitable exists.\n */\nexport function pickDisplayField(\n fields: Record<string, FieldMetaLite> | undefined,\n): string | undefined {\n if (!fields) return undefined;\n for (const preferred of ['name', 'title', 'label']) {\n if (fields[preferred]) return preferred;\n }\n for (const [name, meta] of Object.entries(fields)) {\n if (meta.type === 'text' || meta.type === 'string') return name;\n }\n return undefined;\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n//\n// ADR-0037 Phase 3 — draft data preview: evaluate an AnalyticsQuery over an\n// in-memory row set (the pending `seed` draft's records) instead of the real\n// data engine. This is what lets a Live Canvas dashboard chart REAL numbers\n// from the DRAFTED sample data before anything is published — and because\n// publish materializes the *same* seed, the numbers are continuous across\n// the publish boundary.\n//\n// Scope (deliberately the dataset-query subset, not a general engine):\n// • Mongo-style `where` filters ($eq implicit, $ne/$gt/$gte/$lt/$lte/\n// $between/$in/$nin/$contains, $and/$or/$not)\n// • timeDimensions date-range filtering + granularity bucketing\n// (day/week/month/quarter/year)\n// • group-by dimensions; count / countDistinct / sum / avg / min / max\n// • order + limit/offset\n// Anything beyond (joins via `include`, raw SQL) falls back to the caller's\n// normal execution path — the preview simply doesn't claim it.\n\nimport { calendarPartsInTzOrUtc, nextUtcCalendarDay, utcInstantMs } from '@objectstack/core';\nimport type { AnalyticsQuery, AnalyticsResult } from '@objectstack/spec/contracts';\nimport type { Cube } from '@objectstack/spec/data';\n\ntype Row = Record<string, unknown>;\n\n// ── Filters (the unified Query DSL subset) ──────────────────────────────────\n\n/**\n * Order two operands the way every other filter backend orders them.\n *\n * The `Date` arm is load-bearing rather than defensive: `String(new Date())`\n * is `'Mon Jul 27 2026 …'`, which under the plain string ordering below sorts\n * AFTER every `'2026-…'` comparand — so a preview row carrying an instant both\n * disappeared from windows it belongs in and appeared in ones it does not.\n * Measured against the shared matrix, 10 of 16 cases diverged, and unlike the\n * cross-type silence on the drivers this direction ADDS rows: a drafted chart\n * showed numbers no published chart would.\n *\n * The population is real. `Field.datetime`'s storage form is a BSON `Date` on\n * `driver-mongodb` (ADR-0053 D-E2), so rows fetched from a mongo-backed dataset\n * arrive here as `Date` objects, while the comparands are wire text.\n * {@link utcInstantMs} is the same primitive `formula`'s write-side evaluator\n * uses for the same pairing, so the two type-blind surfaces cannot drift.\n *\n * Deliberately narrow: the lift runs only when one side is a `Date` and both\n * read as instants, so string-vs-string keeps ISO lexicographic ordering and a\n * `Field.time` wall clock — which denotes no instant — is left untouched.\n */\nfunction compare(a: unknown, b: unknown): number {\n if (typeof a === 'number' && typeof b === 'number') return a - b;\n if (a instanceof Date || b instanceof Date) {\n const ai = utcInstantMs(a);\n const bi = utcInstantMs(b);\n if (ai !== null && bi !== null) return ai - bi;\n }\n return String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0;\n}\n\n/**\n * The inclusive-upper-bound comparison, with the calendar-day rule (#3777): a\n * bare-day bound means \"through that whole day\", so it is evaluated half-open\n * against the next day. String ordering makes `< nextDay` equivalent to\n * `<= day` for plain date values, so this needs no field-type lookup — which\n * matters here, because the preview sees drafted rows with no schema.\n *\n * Shared by `$lte` and the max of `$between` so the two cannot drift apart.\n */\nfunction lteBound(value: unknown, bound: unknown): boolean {\n const nextDay = nextUtcCalendarDay(bound);\n if (nextDay != null) return compare(value, nextDay) < 0;\n return compare(value, bound) <= 0;\n}\n\nfunction matchOp(value: unknown, op: string, expected: unknown): boolean {\n switch (op) {\n case '$eq': return value === expected || String(value) === String(expected);\n case '$ne': return !(value === expected || String(value) === String(expected));\n case '$gt': return value != null && compare(value, expected) > 0;\n case '$gte': return value != null && compare(value, expected) >= 0;\n case '$lt': return value != null && compare(value, expected) < 0;\n case '$lte': {\n if (value == null) return false;\n // A bare-day upper bound means \"through that whole day\" (#3777): the SQL\n // paths compile it half-open (`< day+1`), and the preview must agree or\n // a drafted chart shows different numbers than the published one. String\n // ordering makes `< nextDay` equivalent to `<= day` for plain date\n // values, so no type lookup is needed here either.\n return lteBound(value, expected);\n }\n case '$between': {\n // Was absent, so it fell to the permissive `default` and matched EVERY\n // row — a drafted chart with a range filter silently charted the whole\n // dataset, then changed at publish (found by the ADR-0053 D-A3 matrix,\n // #4081). The max takes the same whole-day rule as `$lte`.\n if (value == null || !Array.isArray(expected) || expected.length !== 2) return false;\n const [min, max] = expected;\n if (min == null || max == null) return false;\n return compare(value, min) >= 0 && lteBound(value, max);\n }\n case '$in': return Array.isArray(expected) && expected.some((e) => value === e || String(value) === String(e));\n case '$nin': return Array.isArray(expected) && !expected.some((e) => value === e || String(value) === String(e));\n case '$contains': return String(value ?? '').toLowerCase().includes(String(expected ?? '').toLowerCase());\n default: return true; // unknown operator — permissive (preview, reads only)\n }\n}\n\nexport function matchesWhere(row: Row, where: Record<string, unknown> | undefined): boolean {\n if (!where) return true;\n for (const [key, cond] of Object.entries(where)) {\n if (key === '$and') {\n if (!(cond as Row[]).every((c) => matchesWhere(row, c as Row))) return false;\n } else if (key === '$or') {\n if (!(cond as Row[]).some((c) => matchesWhere(row, c as Row))) return false;\n } else if (key === '$not') {\n if (matchesWhere(row, cond as Row)) return false;\n } else if (cond !== null && typeof cond === 'object' && !Array.isArray(cond)) {\n for (const [op, expected] of Object.entries(cond as Row)) {\n if (!matchOp(row[key], op, expected)) return false;\n }\n } else if (!(row[key] === cond || String(row[key]) === String(cond))) {\n return false; // implicit equality\n }\n }\n return true;\n}\n\n// ── Time bucketing ──────────────────────────────────────────────────────────\n\nexport function bucketDate(value: unknown, granularity: string, timezone?: string): string | null {\n const d = new Date(String(value));\n if (Number.isNaN(d.getTime())) return null;\n // ADR-0053 Phase 2: resolve the calendar day in the reference zone so an\n // instant near a tz day-boundary buckets where a user in that zone expects.\n // Unset / 'UTC' / invalid keeps the historical UTC bucketing.\n const { year: y, month, day: dayNum } = calendarPartsInTzOrUtc(d, timezone);\n const m = `${month}`.padStart(2, '0');\n const day = `${dayNum}`.padStart(2, '0');\n switch (granularity) {\n case 'year': return `${y}`;\n case 'quarter': return `${y}-Q${Math.floor((month - 1) / 3) + 1}`;\n case 'month': return `${y}-${m}`;\n case 'week': {\n // Build a UTC date from the zone-shifted parts, then step back to Monday.\n const monday = new Date(Date.UTC(y, month - 1, dayNum));\n const dow = (monday.getUTCDay() + 6) % 7; // Monday=0\n monday.setUTCDate(monday.getUTCDate() - dow);\n return monday.toISOString().slice(0, 10);\n }\n case 'day':\n default:\n return `${y}-${m}-${day}`;\n }\n}\n\n// ── Aggregation ─────────────────────────────────────────────────────────────\n\nfunction aggregate(rows: Row[], metricType: string, field: string): number {\n if (metricType === 'count' || field === '*') {\n if (metricType === 'countDistinct') {\n return new Set(rows.map((r) => r[field]).filter((v) => v != null)).size;\n }\n return rows.length;\n }\n const nums = rows.map((r) => Number(r[field])).filter((n) => Number.isFinite(n));\n switch (metricType) {\n case 'countDistinct': return new Set(rows.map((r) => r[field]).filter((v) => v != null)).size;\n case 'sum': return nums.reduce((a, b) => a + b, 0);\n case 'avg': return nums.length ? nums.reduce((a, b) => a + b, 0) / nums.length : 0;\n case 'min': return nums.length ? Math.min(...nums) : 0;\n case 'max': return nums.length ? Math.max(...nums) : 0;\n default: return nums.length ? nums.reduce((a, b) => a + b, 0) : rows.length;\n }\n}\n\n/**\n * Evaluate `query` over `rows` using the cube's measure/dimension specs.\n * Mirrors the engine strategies' output contract: rows keyed by bare\n * measure/dimension names, `fields` describing each output column.\n */\nexport function evaluateAnalyticsQueryOverRows(\n query: AnalyticsQuery,\n cube: Cube,\n rows: Row[],\n): AnalyticsResult {\n // 1. Row-level filters: `where`, then timeDimension dateRanges.\n let filtered = rows.filter((r) => matchesWhere(r, query.where));\n const timeDims = query.timeDimensions ?? [];\n for (const td of timeDims) {\n const dim = cube.dimensions?.[td.dimension];\n const field = String(dim?.sql ?? td.dimension);\n if (!td.dateRange) continue;\n const [start, end] = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];\n filtered = filtered.filter((r) => {\n const v = String(r[field] ?? '');\n // Bare-day end → half-open `< day+1`, the same translation the SQL\n // paths apply (#3777); a full-timestamp end keeps the historical\n // `'~'`-suffix trick (inclusive of that instant's own sub-values).\n const nextDay = nextUtcCalendarDay(end);\n const inUpper = nextDay != null ? v < nextDay : v <= `${end}~`;\n return v >= String(start) && inUpper;\n });\n }\n\n // 2. Grouping keys: each selected dimension (time dims bucketed).\n const dimensions = query.dimensions ?? [];\n const timezone = query.timezone; // ADR-0053 Phase 2: reference tz for bucketing\n const granByDim = new Map(timeDims.filter((t) => t.granularity).map((t) => [t.dimension, t.granularity!]));\n const keyOf = (r: Row): { key: string; values: Row } => {\n const values: Row = {};\n for (const name of dimensions) {\n const dim = cube.dimensions?.[name];\n const field = String(dim?.sql ?? name);\n const raw = r[field];\n const gran = granByDim.get(name) ?? (dim?.type === 'time' && dim.granularities?.length === 1 ? String(dim.granularities[0]) : undefined);\n values[name] = gran ? bucketDate(raw, gran, timezone) : (raw ?? null);\n }\n return { key: JSON.stringify(values), values };\n };\n\n const groups = new Map<string, { values: Row; rows: Row[] }>();\n for (const r of filtered) {\n const { key, values } = keyOf(r);\n const g = groups.get(key) ?? { values, rows: [] };\n g.rows.push(r);\n groups.set(key, g);\n }\n // No dimensions → a single overall group (even over zero rows: count = 0).\n if (dimensions.length === 0 && groups.size === 0) {\n groups.set('{}', { values: {}, rows: [] });\n }\n\n // 3. Aggregate each measure per group.\n const out: Row[] = [];\n for (const g of groups.values()) {\n const row: Row = { ...g.values };\n for (const m of query.measures) {\n const metric = cube.measures?.[m];\n row[m] = aggregate(g.rows, String(metric?.type ?? 'count'), String(metric?.sql ?? '*'));\n }\n out.push(row);\n }\n\n // 4. Order + paging.\n for (const [col, dir] of Object.entries(query.order ?? {}).reverse()) {\n out.sort((a, b) => (dir === 'desc' ? -1 : 1) * compare(a[col], b[col]));\n }\n const offset = query.offset ?? 0;\n const limited = out.slice(offset, query.limit != null ? offset + query.limit : undefined);\n\n return {\n rows: limited,\n fields: [\n ...dimensions.map((d) => ({ name: d, type: 'string' })),\n ...query.measures.map((m) => ({ name: m, type: 'number' })),\n ],\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Cube, FilterCondition } from '@objectstack/spec/data';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\nimport type { IAnalyticsService, IDataDriver } from '@objectstack/spec/contracts';\nimport { AnalyticsService } from './analytics-service.js';\nimport type { AnalyticsServiceConfig } from './analytics-service.js';\nimport type { AnalyticsDriverCapabilities } from './strategies/types.js';\nimport { pickDisplayField, type DimensionLabelDeps } from './dimension-labels.js';\n\n/**\n * Minimal IDataEngine surface required for the auto-bridge.\n * ObjectQL exposes:\n * - `aggregate(object, { where, groupBy, aggregations: [{ function, field, alias }] })`\n * - `execute(sql, options)` for raw SQL pass-through (enables NativeSQLStrategy\n * and lets the analytics layer emit JOINs for relation traversal).\n */\ninterface DataEngineLike {\n aggregate(object: string, options: {\n where?: Record<string, unknown>;\n groupBy?: string[];\n aggregations?: Array<{ function: string; field: string; alias: string }>;\n /** Reference timezone (IANA) for date bucketing — ADR-0053 Phase 2. */\n timezone?: string;\n /**\n * `BaseEngineOptions.context` — identity/tenant of the request. The engine\n * merges it into the operation context (`mergeReadContext`), which is what\n * lets its middleware chain inject RLS into `opCtx.ast.where` (#3602).\n */\n context?: ExecutionContext;\n }): Promise<unknown[]>;\n execute?(command: unknown, options?: Record<string, unknown>): Promise<unknown>;\n /** Return the registered object schema (relationship → target + display-label resolution). */\n getObject?(name: string): {\n fields?: Record<string, {\n type?: string;\n reference?: string;\n options?: Array<{ value: unknown; label?: string }>;\n }>;\n /** Federation marker (ADR-0015): set on objects bound to an external datasource. */\n external?: unknown;\n /** The datasource this object is bound to (ADR-0062 D6 external detection). */\n datasource?: string;\n } | undefined;\n /**\n * Resolve the storage driver backing an object (public ObjectQL accessor).\n * Used to delegate temporal storage-form coercion to the driver, which is the\n * single source of truth for how a `Field.date`/`Field.datetime` is stored on\n * the active dialect. When the hooks are absent, values and column SQL pass\n * through untouched — the contract's identity semantics.\n */\n getDriverForObject?(objectName: string): TemporalDriverSurface | undefined;\n}\n\n/**\n * The slice of the `IDataDriver` CONTRACT the analytics layer consumes —\n * `temporalFilterValue` / `temporalFilterColumnSql` are first-class contract\n * members since ADR-0053 D-A2, no longer a duck-typed local invention. Picked\n * (rather than using `IDataDriver` whole) because `getDriverForObject` hands\n * back whatever the engine registered, and this seam only needs the temporal\n * surface; the runtime `typeof` guards below remain the correct way to consume\n * an optional contract member.\n */\ntype TemporalDriverSurface = Pick<\n IDataDriver,\n 'temporalFilterValue' | 'temporalFilterColumnSql'\n>;\n\n/**\n * Configuration for AnalyticsServicePlugin.\n */\nexport interface AnalyticsServicePluginOptions {\n /** Pre-defined cube definitions (from manifest). */\n cubes?: Cube[];\n /**\n * Probe driver capabilities for a given cube.\n * When omitted, defaults to in-memory only.\n */\n queryCapabilities?: (cubeName: string) => AnalyticsDriverCapabilities;\n /**\n * Execute raw SQL on a driver. Enables NativeSQLStrategy.\n */\n executeRawSql?: (objectName: string, sql: string, params: unknown[]) => Promise<Record<string, unknown>[]>;\n /**\n * Execute ObjectQL aggregate. Enables ObjectQLStrategy.\n */\n executeAggregate?: (objectName: string, options: {\n groupBy?: string[];\n aggregations?: Array<{ field: string; method: string; alias: string }>;\n filter?: Record<string, unknown>;\n /** Reference timezone (IANA) for date bucketing — ADR-0053 Phase 2. */\n timezone?: string;\n /**\n * ADR-0021 D-C (#3602) — the request's ExecutionContext. A custom bridge\n * MUST forward it to its engine so engine-side RLS applies; dropping it is\n * what made the built-in bridge fall open in #3597.\n */\n context?: ExecutionContext;\n }) => Promise<Record<string, unknown>[]>;\n /**\n * ADR-0021 D-C — context-aware per-object read scope (tenant + RLS). The\n * runtime supplies this from its sharing middleware so the analytics raw-SQL\n * path cannot bypass tenant isolation. Receives the request's ExecutionContext\n * and returns the RLS `FilterCondition` for the object (what `RLSCompiler`\n * emits). When omitted, the plugin auto-bridges to a registered `'security'`\n * service exposing `getReadFilter(object, context)` if one is present.\n */\n getReadScope?: (\n objectName: string,\n context?: ExecutionContext,\n ) =>\n | FilterCondition\n | null\n | undefined\n | Promise<FilterCondition | null | undefined>;\n /**\n * ADR-0021 D-C — join allowlist per cube (the dataset's declared `include`).\n * Typically wired from the dataset registry's compiled `allowedRelationships`.\n */\n getAllowedRelationships?: (cubeName: string) => Set<string> | undefined;\n /** Enable debug logging. */\n debug?: boolean;\n}\n\n/**\n * AnalyticsServicePlugin — Kernel plugin for multi-driver analytics.\n *\n * Lifecycle:\n * 1. **init** — Creates `AnalyticsService`, registers as `'analytics'` service.\n * If an existing analytics service is already registered (e.g. MemoryAnalyticsService\n * from dev-plugin), it is captured as the `fallbackService`.\n * 2. **start** — Triggers `'analytics:ready'` hook so other plugins can\n * register cubes or extend the service.\n * 3. **destroy** — Cleans up references.\n *\n * @example\n * ```ts\n * import { LiteKernel } from '@objectstack/core';\n * import { AnalyticsServicePlugin } from '@objectstack/service-analytics';\n *\n * const kernel = new LiteKernel();\n * kernel.use(new AnalyticsServicePlugin({\n * cubes: [ordersCube],\n * queryCapabilities: (cube) => ({ nativeSql: true, objectqlAggregate: true, inMemory: false }),\n * executeRawSql: async (obj, sql, params) => pgPool.query(sql, params).then(r => r.rows),\n * }));\n * await kernel.bootstrap();\n *\n * const analytics = kernel.getService<IAnalyticsService>('analytics');\n * const result = await analytics.query({ cube: 'orders', measures: ['orders.count'] });\n * ```\n */\nexport class AnalyticsServicePlugin implements Plugin {\n name = 'com.objectstack.service-analytics';\n /**\n * Services init() registers on every path (ADR-0116, #4131) — lets the\n * kernel name this plugin when a consumer requires one before it inits.\n */\n providesServices = ['analytics'];\n version = '1.0.0';\n type = 'standard' as const;\n dependencies: string[] = [];\n /**\n * init() probes the `data` engine ObjectQLPlugin provides for the\n * auto-bridge — order-if-present so the probe verdict is deterministic\n * (ADR-0116, #4471). Soft, not hard: without an engine the plugin\n * degrades on purpose (per-query lazy resolution / explicit\n * `executeAggregate`).\n */\n optionalDependencies: string[] = ['com.objectstack.engine.objectql'];\n\n private service?: AnalyticsService;\n private readonly options: AnalyticsServicePluginOptions;\n\n constructor(options: AnalyticsServicePluginOptions = {}) {\n this.options = options;\n }\n\n async init(ctx: PluginContext): Promise<void> {\n // Check if there is an existing analytics service (e.g. from dev-plugin)\n let fallbackService: IAnalyticsService | undefined;\n try {\n const existing = ctx.getService<IAnalyticsService>('analytics');\n if (existing && typeof existing.query === 'function') {\n fallbackService = existing;\n ctx.logger.debug('[Analytics] Found existing analytics service, using as fallback');\n }\n } catch {\n // No existing service — that's fine\n }\n\n // Auto-bridge: when caller did not supply executeAggregate, look up the\n // kernel's IDataEngine (registered as 'data' by ObjectQLPlugin) lazily and\n // translate AnalyticsStrategy's `{method, filter}` shape into the engine's\n // `{function, where}` shape. This lets users write\n // `new AnalyticsServicePlugin({ cubes })`\n // without re-implementing the bridge in every app.\n let executeAggregate = this.options.executeAggregate;\n let autoBridged = false;\n if (!executeAggregate) {\n const tryGetDataEngine = (): DataEngineLike | undefined => {\n try {\n const svc = ctx.getService<DataEngineLike>('data');\n return svc && typeof svc.aggregate === 'function' ? svc : undefined;\n } catch {\n return undefined;\n }\n };\n // Probe now (warn if missing) but resolve at call time so plugin order\n // does not matter as long as 'data' exists by the time a query runs.\n if (!tryGetDataEngine()) {\n ctx.logger.warn(\n '[Analytics] No \"data\" service registered yet at init; ' +\n 'will retry per-query. Register ObjectQLPlugin or pass executeAggregate.',\n );\n }\n executeAggregate = async (objectName, { groupBy, aggregations, filter, timezone, context }) => {\n const engine = tryGetDataEngine();\n if (!engine) {\n throw new Error(\n '[Analytics] Cannot execute aggregate: no IDataEngine (\"data\") service is registered. ' +\n 'Add ObjectQLPlugin to the kernel or supply AnalyticsServicePlugin({ executeAggregate }).',\n );\n }\n const rows = await engine.aggregate(objectName, {\n where: filter,\n groupBy,\n aggregations: aggregations?.map((a) => ({\n function: a.method,\n field: a.field,\n alias: a.alias,\n })),\n // ADR-0053 Phase 2: thread the reference tz so date buckets resolve on\n // that zone's calendar days (engine buckets in-memory when non-UTC).\n timezone,\n // ADR-0021 D-C (#3602): thread the caller's identity so the engine's\n // middleware chain scopes the read itself. `BaseEngineOptions.context`\n // is `.optional()`, so nothing ever forced this bridge to pass it —\n // and it did not, which is how an authenticated aggregate reached the\n // engine with no principal and plugin-security fell open (#3597).\n context,\n });\n return rows as Record<string, unknown>[];\n };\n autoBridged = true;\n }\n\n // Auto-bridge raw SQL when the data engine exposes `execute()` and the\n // caller did not supply their own `executeRawSql`. This unlocks\n // NativeSQLStrategy (priority 10) which can emit `LEFT JOIN`s for\n // dotted dimension/measure references like `account.industry`.\n let executeRawSql = this.options.executeRawSql;\n let autoBridgedRawSql = false;\n if (!executeRawSql) {\n const tryGetExecutor = (): DataEngineLike | undefined => {\n try {\n const svc = ctx.getService<DataEngineLike>('data');\n return svc && typeof svc.execute === 'function' ? svc : undefined;\n } catch {\n return undefined;\n }\n };\n // Always wire the bridge — resolution happens at call time, mirroring\n // the executeAggregate auto-bridge above. This way plugin-init order\n // does not matter as long as `data` exists by the time a query runs.\n executeRawSql = async (_objectName, sql, params) => {\n const engine = tryGetExecutor();\n if (!engine || !engine.execute) {\n throw new Error(\n '[Analytics] Cannot execute raw SQL: no IDataEngine (\"data\") service with execute() is registered.',\n );\n }\n // NativeSQLStrategy emits `$1, $2, …` placeholders. Knex (used by\n // driver-sql) speaks `?` placeholders, so translate.\n const knexSql = sql.replace(/\\$(\\d+)/g, '?');\n const result = await engine.execute(knexSql, { args: params });\n // A driver that cannot run SQL (e.g. the in-memory driver) returns\n // null from execute(). Silently mapping that to [] made EVERY dataset\n // query on such environments report \"No rows\" while looking healthy\n // (HTTP 200, compiled SQL attached). Throw a TYPED error instead so\n // the orchestrator can fall back to an aggregate-based strategy —\n // never fabricate an empty result.\n if (result === null || result === undefined) {\n const err = new Error(\n '[Analytics] The \"data\" engine\\'s driver returned null for raw SQL — ' +\n 'this driver does not support SQL execution. The query will fall back ' +\n 'to an aggregate-based strategy when one is available.',\n ) as Error & { code: string };\n err.code = 'RAW_SQL_UNSUPPORTED';\n throw err;\n }\n if (Array.isArray(result)) return result as Record<string, unknown>[];\n if (typeof result === 'object' && 'rows' in (result as Record<string, unknown>)) {\n return (result as { rows: Record<string, unknown>[] }).rows;\n }\n return [];\n };\n autoBridgedRawSql = true;\n }\n\n // Default capabilities: when we have an aggregate bridge, advertise\n // ObjectQL support so ObjectQLStrategy is selected. Callers can still\n // override via options.queryCapabilities.\n const queryCapabilities = this.options.queryCapabilities\n ?? (() => ({\n nativeSql: !!executeRawSql,\n objectqlAggregate: !!executeAggregate,\n inMemory: false,\n }));\n\n // ADR-0021 D-C — wire the read-scope provider. Prefer an explicit option;\n // otherwise auto-bridge to a registered `'security'` service that exposes\n // `getReadFilter(object, context)` (resolved at call time so plugin-init\n // order does not matter). This keeps analytics decoupled from security.\n interface SecurityReadFilter {\n getReadFilter(\n object: string,\n context?: ExecutionContext,\n ):\n | FilterCondition\n | null\n | undefined\n | Promise<FilterCondition | null | undefined>;\n }\n let getReadScope = this.options.getReadScope;\n let autoBridgedReadScope = false;\n let securityPresentAtInit = false;\n if (!getReadScope) {\n const trySecurity = (): SecurityReadFilter | undefined => {\n try {\n const svc = ctx.getService<SecurityReadFilter>('security');\n return svc && typeof svc.getReadFilter === 'function' ? svc : undefined;\n } catch {\n return undefined;\n }\n };\n // ALWAYS wire the bridge — resolution happens at call time, mirroring the\n // executeAggregate / executeRawSql auto-bridges above. Gating the\n // ASSIGNMENT on an init-time probe (as this did) made analytics RLS\n // silently plugin-ORDER-DEPENDENT: a kernel that registers this plugin\n // before the security plugin got NO read-scope provider at all, so every\n // strategy ran unscoped and only a WARN marked it. The repo's own\n // `bootStack` harness registers in exactly that order, which is why no\n // dogfood test could ever observe analytics RLS.\n securityPresentAtInit = !!trySecurity();\n getReadScope = (object, context) => trySecurity()?.getReadFilter(object, context);\n autoBridgedReadScope = true;\n }\n\n // ADR-0021 — relationship → target-object resolver. A dataset's `include`\n // names lookup/master_detail FIELDS on the base object; the joined TABLE is\n // each field's `reference` target (which can differ from the field name,\n // e.g. lookup `account` → object `crm_account`). Resolve from the 'data'\n // engine's object schema at compile time so cross-object joins target the\n // right table. Resolved lazily so plugin-init order doesn't matter.\n const relationshipResolver = (baseObject: string, relationshipName: string): string | undefined => {\n const engine = (() => {\n try {\n const svc = ctx.getService<DataEngineLike>('data');\n return svc && typeof svc.getObject === 'function' ? svc : undefined;\n } catch { return undefined; }\n })();\n const obj = engine?.getObject?.(baseObject);\n const field = obj?.fields?.[relationshipName];\n if (field && (field.type === 'lookup' || field.type === 'master_detail') && field.reference) {\n return field.reference;\n }\n // Unknown to the schema — fall back to the relationship name as the table\n // (legacy same-name convention). Returning undefined would make the\n // compiler reject the dataset; the name-as-table fallback is safer for\n // engines that don't expose getObject.\n return engine ? undefined : relationshipName;\n };\n\n // ADR-0021 — dimension display-label resolution. `queryDataset` groups by a\n // dimension's raw stored value; for `select` fields the user-facing text is\n // the option label, and for `lookup`/`master_detail` fields it's the related\n // record's display name. Wire the two low-level capabilities the resolver\n // needs from the 'data' engine (resolved lazily so plugin-init order is free):\n // - field metadata (select options + lookup target), via getObject\n // - id→name pairs, via the executeAggregate bridge (group by id + name)\n const dataEngine = (): DataEngineLike | undefined => {\n try {\n const svc = ctx.getService<DataEngineLike>('data');\n return svc && typeof svc.getObject === 'function' ? svc : undefined;\n } catch { return undefined; }\n };\n const labelResolver: DimensionLabelDeps = {\n getObjectFields: (objectName) => dataEngine()?.getObject?.(objectName)?.fields,\n fetchRecordLabels: async (targetObject, ids, scope, context) => {\n const map = new Map<unknown, string>();\n const displayField = pickDisplayField(dataEngine()?.getObject?.(targetObject)?.fields);\n if (!displayField || !executeAggregate || ids.length === 0) return map;\n // #3680 — the sort-key pass hands over the PRE-window id set (every\n // grouped value, not just the displayed page), so a high-cardinality\n // lookup dimension can push thousands of ids through here. Chunk the\n // `$in` so the bound-parameter count stays under every driver's limit\n // (SQLite's historic floor is 999 variables).\n const CHUNK = 500;\n for (let i = 0; i < ids.length; i += CHUNK) {\n // #3602 — AND the referenced object's own read scope into the id filter,\n // with `$and` (never key-merge) so it cannot be displaced by the id\n // predicate — the same composition the strategy uses for the aggregate.\n // Without it this per-record read leaks display names the target's RLS\n // would hide (fires when the referenced object is stricter than the base).\n const idFilter: Record<string, unknown> = { id: { $in: ids.slice(i, i + CHUNK) } };\n const filter = scope ? { $and: [idFilter, scope] } : idFilter;\n // Group by (id, displayField) — one row per record — reusing the aggregate\n // bridge rather than adding a record-fetch capability. A count keeps engines\n // that require ≥1 aggregation happy; the count itself is unused.\n const rows = await executeAggregate(targetObject, {\n groupBy: ['id', displayField],\n aggregations: [{ field: 'id', method: 'count', alias: '_c' }],\n filter,\n // #3602 second belt — `scope` above is the analytics layer's own\n // predicate on this per-record read; the context makes the engine's\n // middleware scope it as well.\n context,\n });\n for (const r of rows) {\n if (r.id != null && r[displayField] != null) map.set(r.id, String(r[displayField]));\n }\n }\n return map;\n },\n };\n\n // ADR-0037 P3 — draft data preview: resolve the PENDING seed draft's rows\n // for an object via the kernel protocol (state:'draft' read — a published\n // seed's rows are already in the real table and must NOT overlay). Lazy\n // service lookup so plugin order doesn't matter; null ⇒ no pending seed ⇒\n // queryDataset falls through to live data.\n const draftRowsResolver = async (objectName: string): Promise<Record<string, unknown>[] | null> => {\n type ProtocolLike = {\n getMetaItems?(req: { type: string; previewDrafts?: boolean }): Promise<unknown>;\n getMetaItem?(req: { type: string; name: string; state?: string }): Promise<unknown>;\n };\n let protocol: ProtocolLike | undefined;\n try {\n protocol = ctx.getService<ProtocolLike>('protocol');\n } catch { return null; }\n if (!protocol?.getMetaItems || !protocol.getMetaItem) return null;\n const res = await protocol.getMetaItems({ type: 'seed', previewDrafts: true }).catch(() => null);\n const list = Array.isArray(res)\n ? res\n : (res && typeof res === 'object' && Array.isArray((res as { items?: unknown[] }).items)\n ? (res as { items: unknown[] }).items\n : []);\n const rows: Record<string, unknown>[] = [];\n let pending = false;\n for (const entry of list) {\n const body = ((entry as { item?: unknown })?.item ?? entry) as { name?: string; object?: string } | null;\n if (!body?.name || body.object !== objectName) continue;\n // Only a PENDING draft row qualifies; getMetaItem({state:'draft'})\n // throws no_draft when the seed is already published.\n const draft = await protocol.getMetaItem({ type: 'seed', name: body.name, state: 'draft' }).catch(() => null);\n const draftBody = (draft as { item?: { records?: unknown[] } } | null)?.item;\n if (!draftBody) continue;\n pending = true;\n for (const r of Array.isArray(draftBody.records) ? draftBody.records : []) {\n if (r && typeof r === 'object') rows.push(r as Record<string, unknown>);\n }\n }\n return pending ? rows : null;\n };\n\n // Temporal storage-form coercion (fixes the SQLite datetime \"No rows\" bug).\n // The raw-SQL strategy binds dashboard relative-date tokens (already expanded\n // to ISO strings) directly, bypassing the driver's CRUD coercion. Delegate to\n // the driver — the single source of truth for the on-disk storage convention —\n // so a `Field.datetime` ISO comparand becomes epoch ms on SQLite, while\n // `Field.date` text and native-timestamp (Postgres) columns pass through\n // unchanged. Resolved at call time so plugin-init order does not matter.\n const coerceTemporalFilterValue = (\n objectName: string,\n fieldName: string,\n value: unknown,\n ): unknown => {\n try {\n const svc = ctx.getService<DataEngineLike>('data');\n const driver = svc?.getDriverForObject?.(objectName);\n if (driver && typeof driver.temporalFilterValue === 'function') {\n return driver.temporalFilterValue(objectName, fieldName, value);\n }\n } catch {\n // No data engine / driver, or it doesn't support coercion — leave the\n // value as-is (today's behaviour; safe for text/native-timestamp paths).\n }\n return value;\n };\n\n // The column half of the same fix (#3912). A SQLite `Field.datetime` column\n // holds BOTH storage forms — INTEGER epoch from a `Date` write, ISO TEXT from\n // a REST/JSON write or a `NOW()` default — so coercing the comparand alone\n // matched whichever half the writer produced and returned an empty window for\n // the other. Ask the driver for the column expression that normalises both.\n const coerceTemporalFilterColumn = (\n objectName: string,\n fieldName: string,\n columnSql: string,\n ): string => {\n try {\n const svc = ctx.getService<DataEngineLike>('data');\n const driver = svc?.getDriverForObject?.(objectName);\n if (driver && typeof driver.temporalFilterColumnSql === 'function') {\n return driver.temporalFilterColumnSql(objectName, fieldName, columnSql);\n }\n } catch {\n // Same tiering as above — an unresolvable driver emits the bare column,\n // which is today's behaviour and correct on every non-mixed dialect.\n }\n return columnSql;\n };\n\n const config: AnalyticsServiceConfig = {\n cubes: this.options.cubes,\n logger: ctx.logger,\n queryCapabilities,\n executeRawSql,\n executeAggregate,\n fallbackService,\n getReadScope,\n getAllowedRelationships: this.options.getAllowedRelationships,\n coerceTemporalFilterValue,\n coerceTemporalFilterColumn,\n relationshipResolver,\n labelResolver,\n // Source-field metadata behind the display chains on result columns:\n // ADR-0053 currency (`currencyConfig.defaultCurrency`) and percent scale\n // (`max`, which is what marks whole-percent storage — objectui#3136).\n sourceFieldMeta: (object: string, field: string) => {\n const f = dataEngine()?.getObject?.(object)?.fields?.[field] as\n | { type?: string; max?: number; currencyConfig?: { defaultCurrency?: string } }\n | undefined;\n return f ? { type: f.type, max: f.max, defaultCurrency: f.currencyConfig?.defaultCurrency } : undefined;\n },\n // ADR-0062 D6 — a federated object carries an `external` block (ADR-0015).\n // Reported so NativeSQLStrategy declines it (its hand-compiled FROM would\n // hit the wrong physical table) and the driver-correct ObjectQL path runs.\n isExternalObject: (objectName: string) => {\n const obj = dataEngine()?.getObject?.(objectName);\n return !!(obj && obj.external != null);\n },\n // [#3867] Existence probe for the cube auto-inference gate. Reads the\n // same schema registry the data path's #3770 gate consults, through the\n // engine accessor this bridge already uses above — so \"which objects\n // exist\" has one answer across /data and /analytics.\n //\n // `dataEngine()` resolves lazily and may be absent entirely (analytics\n // installed without a data engine). Reporting `false` there would 404\n // every cube, so an unresolvable engine reports `true` — \"cannot answer,\n // do not block\" — mirroring the tiering #3770 took on the data path.\n isRegisteredObject: (name: string) => {\n const engine = dataEngine();\n if (!engine) return true;\n return engine.getObject?.(name) != null;\n },\n // [#4437] Field names for the measure source-field gate. Read from the\n // SAME schema registry `isRegisteredObject` above consults (and the data\n // path's #4315 gate reads), so \"which fields exist\" has one answer across\n // /data and /analytics. `undefined` — no engine, unknown object, or an\n // object with no field map (an external datasource whose columns are not\n // mirrored locally) — means \"cannot answer\", and the gate stands down.\n getObjectFieldNames: (objectName: string) => {\n const fields = dataEngine()?.getObject?.(objectName)?.fields;\n if (!fields || typeof fields !== 'object') return undefined;\n const names = Object.keys(fields);\n return names.length > 0 ? names : undefined;\n },\n draftRowsResolver,\n };\n\n if (autoBridgedReadScope && securityPresentAtInit) {\n ctx.logger.info('[Analytics] Auto-bridged getReadScope → \"security\" service (getReadFilter)');\n } else if (autoBridgedReadScope) {\n // The bridge IS wired and will resolve at call time — this is only a\n // heads-up that security had not registered yet at our init. It becomes a\n // real problem only if no security service ever appears.\n ctx.logger.info(\n '[Analytics] getReadScope bridged to the \"security\" service; that service is not ' +\n 'registered yet at init and will be resolved per query (plugin order is not significant).',\n );\n } else if (!getReadScope) {\n ctx.logger.warn(\n '[Analytics] No getReadScope configured and no \"security\" service with getReadFilter found — ' +\n 'analytics queries will NOT enforce tenant/RLS scoping (ADR-0021 D-C). ' +\n 'Supply getReadScope or register a security service in multi-tenant deployments.',\n );\n }\n\n if (autoBridged) {\n ctx.logger.info('[Analytics] Auto-bridged executeAggregate → \"data\" service (IDataEngine)');\n }\n if (autoBridgedRawSql) {\n ctx.logger.info('[Analytics] Auto-bridged executeRawSql → \"data\" service (IDataEngine.execute)');\n }\n\n this.service = new AnalyticsService(config);\n\n // Register or replace the analytics service\n if (fallbackService) {\n ctx.replaceService('analytics', this.service);\n } else {\n ctx.registerService('analytics', this.service);\n }\n\n if (this.options.debug) {\n ctx.hook('analytics:beforeQuery', async (query: unknown) => {\n ctx.logger.debug('[Analytics] Before query', { query });\n });\n }\n\n ctx.logger.info('[Analytics] Service initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n if (!this.service) return;\n\n // Notify other plugins that analytics is ready\n await ctx.trigger('analytics:ready', this.service);\n\n ctx.logger.info(\n `[Analytics] Service started with ${this.service.cubeRegistry.size} cubes: ` +\n `${this.service.cubeRegistry.names().join(', ') || '(none)'}`,\n );\n }\n\n async destroy(): Promise<void> {\n this.service = undefined;\n }\n}\n"],"mappings":";AASA,SAAS,sBAAuD;AAIhE,SAAS,cAAc,0BAA0B,6BAA6B;;;ACCvE,IAAM,eAAN,MAAmB;AAAA,EAAnB;AACL,SAAQ,QAAQ,oBAAI,IAAkB;AAAA;AAAA;AAAA,EAGtC,SAAS,MAAkB;AACzB,SAAK,MAAM,IAAI,KAAK,MAAM,IAAI;AAAA,EAChC;AAAA;AAAA,EAGA,YAAY,OAAqB;AAC/B,eAAW,QAAQ,OAAO;AACxB,WAAK,SAAS,IAAI;AAAA,IACpB;AAAA,EACF;AAAA;AAAA,EAGA,IAAI,MAAgC;AAClC,WAAO,KAAK,MAAM,IAAI,IAAI;AAAA,EAC5B;AAAA;AAAA,EAGA,IAAI,MAAuB;AACzB,WAAO,KAAK,MAAM,IAAI,IAAI;AAAA,EAC5B;AAAA;AAAA,EAGA,SAAiB;AACf,WAAO,MAAM,KAAK,KAAK,MAAM,OAAO,CAAC;AAAA,EACvC;AAAA;AAAA,EAGA,QAAkB;AAChB,WAAO,MAAM,KAAK,KAAK,MAAM,KAAK,CAAC;AAAA,EACrC;AAAA;AAAA,EAGA,IAAI,OAAe;AACjB,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA;AAAA,EAGA,QAAc;AACZ,SAAK,MAAM,MAAM;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,gBACE,YACA,QACM;AACN,UAAM,WAAgC;AAAA,MACpC,OAAO;AAAA,QACL,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM;AAAA,QACN,KAAK;AAAA,MACP;AAAA,IACF;AACA,UAAM,aAAkC,CAAC;AAEzC,eAAW,SAAS,QAAQ;AAC1B,YAAM,QAAQ,MAAM,SAAS,MAAM;AAGnC,YAAM,UAAU,KAAK,yBAAyB,MAAM,IAAI;AACxD,iBAAW,MAAM,IAAI,IAAI;AAAA,QACvB,MAAM,MAAM;AAAA,QACZ;AAAA,QACA,MAAM;AAAA,QACN,KAAK,MAAM;AAAA,QACX,GAAI,YAAY,SACZ,EAAE,eAAe,CAAC,OAAO,QAAQ,SAAS,WAAW,MAAM,EAAE,IAC7D,CAAC;AAAA,MACP;AAGA,UAAI,MAAM,SAAS,YAAY,MAAM,SAAS,cAAc,MAAM,SAAS,WAAW;AACpF,iBAAS,GAAG,MAAM,IAAI,MAAM,IAAI;AAAA,UAC9B,MAAM,GAAG,MAAM,IAAI;AAAA,UACnB,OAAO,GAAG,KAAK;AAAA,UACf,MAAM;AAAA,UACN,KAAK,MAAM;AAAA,QACb;AACA,iBAAS,GAAG,MAAM,IAAI,MAAM,IAAI;AAAA,UAC9B,MAAM,GAAG,MAAM,IAAI;AAAA,UACnB,OAAO,GAAG,KAAK;AAAA,UACf,MAAM;AAAA,UACN,KAAK,MAAM;AAAA,QACb;AAAA,MACF;AAAA,IACF;AAEA,UAAM,OAAa;AAAA,MACjB,MAAM;AAAA,MACN,OAAO;AAAA,MACP,KAAK;AAAA,MACL;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,IACV;AAEA,SAAK,SAAS,IAAI;AAClB,WAAO;AAAA,EACT;AAAA,EAEQ,yBAAyB,WAA2B;AAC1D,YAAQ,WAAW;AAAA,MACjB,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AAAA,MACL,KAAK;AACH,eAAO;AAAA,MACT;AACE,eAAO;AAAA,IACX;AAAA,EACF;AACF;;;AC7EA,IAAM,mBAA2C;AAAA,EAC/C,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,MAAM;AAAA,EACN,WAAW;AAAA,EACX,cAAc;AAAA,EACd,aAAa;AAAA,EACb,WAAW;AACb;AAaA,SAAS,iBAAiB,GAAoB;AAC5C,MAAI,KAAK,KAAM,QAAO;AACtB,MAAI,OAAO,MAAM,UAAW,QAAO,IAAI,SAAS;AAChD,MAAI,aAAa,KAAM,QAAO,EAAE,YAAY;AAC5C,MAAI,OAAO,MAAM,SAAU,QAAO,KAAK,UAAU,CAAC;AAClD,SAAO,OAAO,CAAC;AACjB;AAqBA,SAAS,MAAM,UAA+D;AAC5E,MAAI,SAAS,WAAW,EAAG,QAAO;AAClC,MAAI,SAAS,WAAW,EAAG,QAAO,SAAS,CAAC;AAC5C,SAAO,EAAE,MAAM,OAAO,SAAS;AACjC;AASA,SAAS,YAAY,KAAa,KAAsC;AACtE,QAAM,MAA8B,CAAC;AACrC,QAAM,OAAO,CAAC,UAAkB,WAA2B;AACzD,QAAI,KAAK,EAAE,MAAM,QAAQ,QAAQ,KAAK,UAAU,OAAO,CAAC;AAAA,EAC1D;AAEA,MAAI,QAAQ,MAAM;AAChB,SAAK,UAAU,CAAC,CAAC;AACjB,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,QAAQ,YAAY,CAAC,MAAM,QAAQ,GAAG,KAAK,EAAE,eAAe,OAAO;AAC5E,UAAM,UAAU;AAChB,UAAM,SAAS,OAAO,KAAK,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,WAAW,GAAG,CAAC;AACnE,QAAI,OAAO,SAAS,GAAG;AACrB,iBAAW,SAAS,QAAQ;AAgB1B,YAAI,UAAU,YAAY;AACxB,gBAAMA,KAAI,QAAQ,KAAK;AACvB,cAAI,CAAC,MAAM,QAAQA,EAAC,KAAKA,GAAE,WAAW,GAAG;AAKvC,kBAAM,IAAI;AAAA,cACR,8BAA8B,GAAG,+CAC9B,KAAK,UAAUA,EAAC,CAAC;AAAA,YACtB;AAAA,UACF;AACA,eAAK,OAAO,CAAC,iBAAiBA,GAAE,CAAC,CAAC,CAAC,CAAC;AACpC,eAAK,OAAO,CAAC,iBAAiBA,GAAE,CAAC,CAAC,CAAC,CAAC;AACpC;AAAA,QACF;AASA,YAAI,UAAU,WAAW,UAAU,WAAW;AAC5C,gBAAM,SAAS,UAAU,UAAU,QAAQ,KAAK,MAAM,OAAO,QAAQ,KAAK,MAAM;AAChF,eAAK,SAAS,WAAW,OAAO,CAAC,CAAC;AAClC;AAAA,QACF;AAEA,cAAM,SAAS,iBAAiB,KAAK;AACrC,YAAI,CAAC,QAAQ;AAQX,gBAAM,IAAI;AAAA,YACR,4CAA4C,KAAK,SAAS,GAAG,iBAC/C,OAAO,KAAK,gBAAgB,EAAE,KAAK,IAAI,CAAC;AAAA,UAGxD;AAAA,QACF;AACA,cAAM,IAAI,QAAQ,KAAK;AACvB,aAAK,QAAQ,MAAM,QAAQ,CAAC,IAAI,EAAE,IAAI,gBAAgB,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC;AAAA,MACjF;AACA,aAAO;AAAA,IACT;AAGA,eAAW,CAAC,WAAW,SAAS,KAAK,OAAO,QAAQ,OAAO,GAAG;AAC5D,UAAI,KAAK,GAAG,YAAY,GAAG,GAAG,IAAI,SAAS,IAAI,SAAS,CAAC;AAAA,IAC3D;AACA,WAAO;AAAA,EACT;AAGA,MAAI,MAAM,QAAQ,GAAG,EAAG,MAAK,MAAM,IAAI,IAAI,gBAAgB,CAAC;AAAA,MACvD,MAAK,UAAU,CAAC,iBAAiB,GAAG,CAAC,CAAC;AAC3C,SAAO;AACT;AAYA,SAAS,UAAU,MAA4D;AAC7E,QAAM,WAAmC,CAAC;AAE1C,aAAW,CAAC,KAAK,GAAG,KAAK,OAAO,QAAQ,IAAI,GAAG;AAC7C,QAAI,QAAQ,OAAW;AAEvB,QAAI,QAAQ,UAAU,QAAQ,OAAO;AACnC,UAAI,CAAC,MAAM,QAAQ,GAAG,KAAK,IAAI,WAAW,GAAG;AAC3C,cAAM,IAAI;AAAA,UACR,gBAAgB,GAAG;AAAA,QAGrB;AAAA,MACF;AACA,YAAM,WAAW,IACd,IAAI,CAAC,QAAS,OAAO,OAAO,QAAQ,WAAW,UAAU,GAA8B,IAAI,IAAK,EAChG,OAAO,CAAC,MAAiC,MAAM,IAAI;AACtD,UAAI,SAAS,WAAW,EAAG;AAG3B,UAAI,QAAQ,OAAQ,UAAS,KAAK,GAAG,QAAQ;AAAA,UACxC,UAAS,KAAK,SAAS,WAAW,IAAI,SAAS,CAAC,IAAI,EAAE,MAAM,MAAM,UAAU,SAAS,CAAC;AAC3F;AAAA,IACF;AAEA,QAAI,QAAQ,QAAQ;AAClB,YAAM,QAAQ,OAAO,OAAO,QAAQ,WAAW,UAAU,GAA8B,IAAI;AAC3F,UAAI,MAAO,UAAS,KAAK,EAAE,MAAM,OAAO,OAAO,MAAM,CAAC;AACtD;AAAA,IACF;AAEA,QAAI,IAAI,WAAW,GAAG,GAAG;AACvB,YAAM,IAAI;AAAA,QACR,sDAAsD,GAAG;AAAA,MAE3D;AAAA,IACF;AAEA,aAAS,KAAK,GAAG,YAAY,KAAK,GAAG,CAAC;AAAA,EACxC;AAEA,SAAO,MAAM,QAAQ;AACvB;AAMO,SAAS,6BACd,OAC6B;AAC7B,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,QAAM,QAAS,MAA8B;AAC7C,MAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,EAAG,QAAO;AACxE,SAAO,UAAU,KAAgC;AACnD;AAUO,SAAS,oBACd,MAC6B;AAC7B,MAAI,CAAC,KAAM,QAAO,CAAC;AACnB,MAAI,KAAK,SAAS,OAAQ,QAAO,CAAC,EAAE,QAAQ,KAAK,QAAQ,UAAU,KAAK,UAAU,QAAQ,KAAK,OAAO,CAAC;AACvG,MAAI,KAAK,SAAS,MAAO,QAAO,oBAAoB,KAAK,KAAK;AAC9D,SAAO,KAAK,SAAS,QAAQ,mBAAmB;AAClD;AAGA,SAAS,cAAc,GAA+B;AACpD,MAAI,kBAAkB,KAAK,CAAC,GAAG;AAC7B,UAAM,IAAI,OAAO,CAAC;AAClB,QAAI,OAAO,SAAS,CAAC,EAAG,QAAO;AAAA,EACjC;AACA,SAAO;AACT;AASO,SAAS,wBAAwB,GAAoB;AAC1D,MAAI,MAAM,OAAQ,QAAO;AACzB,MAAI,MAAM,QAAS,QAAO;AAC1B,MAAI,MAAM,OAAQ,QAAO;AACzB,SAAO,cAAc,CAAC,KAAK;AAC7B;AAQO,SAAS,6BAA6B,GAAoB;AAC/D,MAAI,MAAM,OAAQ,QAAO;AACzB,MAAI,MAAM,QAAS,QAAO;AAC1B,MAAI,MAAM,OAAQ,QAAO;AACzB,SAAO,cAAc,CAAC,KAAK;AAC7B;;;ACnUA,IAAM,QAAQ;AAEd,SAAS,WAAW,MAAc,MAAsB;AACtD,MAAI,OAAO,SAAS,YAAY,CAAC,MAAM,KAAK,IAAI,GAAG;AACjD,UAAM,IAAI,MAAM,2BAA2B,IAAI,gBAAgB,OAAO,IAAI,CAAC,sDAAiD;AAAA,EAC9H;AACA,SAAO,IAAI,IAAI;AACjB;AAEO,SAAS,yBACd,QACA,OACoC;AACpC,QAAM,cAAc,WAAW,OAAO,OAAO;AAC7C,QAAM,SAAoB,CAAC;AAC3B,QAAM,MAAM,YAAY,QAAQ,aAAa,MAAM;AACnD,SAAO,EAAE,KAAK,OAAO;AACvB;AAGA,SAAS,YAAY,MAAe,QAAgB,QAA2B;AAC7E,MAAI,SAAS,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,IAAI,GAAG;AACpE,UAAM,IAAI,MAAM,oEAAoE;AAAA,EACtF;AACA,QAAM,UAAoB,CAAC;AAC3B,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,IAA+B,GAAG;AAC1E,QAAI,QAAQ,UAAU,QAAQ,OAAO;AACnC,UAAI,CAAC,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,GAAG;AAC/C,cAAM,IAAI,MAAM,qBAAqB,GAAG,6CAA6C;AAAA,MACvF;AACA,YAAM,QAAS,MACZ,IAAI,CAAC,UAAU,YAAY,OAAO,QAAQ,MAAM,CAAC,EACjD,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC;AAC7B,UAAI,MAAM,WAAW,EAAG;AACxB,YAAM,SAAS,QAAQ,SAAS,UAAU;AAC1C,cAAQ,KAAK,IAAI,MAAM,KAAK,MAAM,CAAC,GAAG;AAAA,IACxC,WAAW,QAAQ,QAAQ;AACzB,YAAM,QAAQ,YAAY,OAAO,QAAQ,MAAM;AAC/C,UAAI,MAAO,SAAQ,KAAK,QAAQ,KAAK,GAAG;AAAA,IAC1C,WAAW,IAAI,WAAW,GAAG,GAAG;AAC9B,YAAM,IAAI,MAAM,oDAAoD,GAAG,kBAAkB;AAAA,IAC3F,OAAO;AACL,cAAQ,KAAK,aAAa,KAAK,OAAO,QAAQ,MAAM,CAAC;AAAA,IACvD;AAAA,EACF;AACA,SAAO,QAAQ,KAAK,OAAO;AAC7B;AAGA,SAAS,aAAa,OAAe,OAAgB,QAAgB,QAA2B;AAC9F,QAAM,MAAM,GAAG,MAAM,IAAI,WAAW,OAAO,OAAO,CAAC;AAGnD,MAAI,UAAU,KAAM,QAAO,GAAG,GAAG;AACjC,MAAI,OAAO,UAAU,YAAY,iBAAiB,MAAM;AACtD,WAAO,KAAK,KAAK;AACjB,WAAO,GAAG,GAAG;AAAA,EACf;AACA,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,UAAM,IAAI,MAAM,0CAA0C,KAAK,4CAAuC;AAAA,EACxG;AAEA,QAAM,MAAM;AACZ,QAAM,OAAO,OAAO,KAAK,GAAG;AAG5B,MAAI,KAAK,WAAW,KAAK,KAAK,KAAK,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,CAAC,GAAG;AAC7D,UAAM,IAAI,MAAM,qBAAqB,KAAK,qFAAqF;AAAA,EACjI;AAEA,QAAM,QAAkB,CAAC;AACzB,aAAW,MAAM,MAAM;AACrB,UAAM,KAAK,gBAAgB,KAAK,IAAI,IAAI,EAAE,GAAG,OAAO,MAAM,CAAC;AAAA,EAC7D;AACA,SAAO,MAAM,WAAW,IAAI,MAAM,CAAC,IAAI,IAAI,MAAM,KAAK,OAAO,CAAC;AAChE;AAEA,SAAS,KAAK,QAAmB,GAAoB;AACnD,SAAO,KAAK,CAAC;AACb,SAAO;AACT;AAEA,SAAS,gBAAgB,KAAa,IAAY,KAAc,OAAe,QAA2B;AACxG,UAAQ,IAAI;AAAA,IACV,KAAK;AAAO,aAAO,QAAQ,OAAO,GAAG,GAAG,aAAa,GAAG,GAAG,MAAM,KAAK,QAAQ,GAAG,CAAC;AAAA,IAClF,KAAK;AAAO,aAAO,QAAQ,OAAO,GAAG,GAAG,iBAAiB,GAAG,GAAG,OAAO,KAAK,QAAQ,GAAG,CAAC;AAAA,IACvF,KAAK;AAAO,aAAO,GAAG,GAAG,MAAM,KAAK,QAAQ,GAAG,CAAC;AAAA,IAChD,KAAK;AAAQ,aAAO,GAAG,GAAG,OAAO,KAAK,QAAQ,GAAG,CAAC;AAAA,IAClD,KAAK;AAAO,aAAO,GAAG,GAAG,MAAM,KAAK,QAAQ,GAAG,CAAC;AAAA,IAChD,KAAK;AAAQ,aAAO,GAAG,GAAG,OAAO,KAAK,QAAQ,GAAG,CAAC;AAAA,IAClD,KAAK,OAAO;AACV,UAAI,CAAC,MAAM,QAAQ,GAAG,EAAG,OAAM,IAAI,MAAM,6BAA6B,KAAK,iCAAiC;AAC5G,UAAI,IAAI,WAAW,EAAG,QAAO;AAC7B,aAAO,GAAG,GAAG,QAAQ,IAAI,IAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC;AAAA,IACjE;AAAA,IACA,KAAK,QAAQ;AACX,UAAI,CAAC,MAAM,QAAQ,GAAG,EAAG,OAAM,IAAI,MAAM,8BAA8B,KAAK,iCAAiC;AAC7G,UAAI,IAAI,WAAW,EAAG,QAAO;AAC7B,aAAO,GAAG,GAAG,YAAY,IAAI,IAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC;AAAA,IACrE;AAAA,IACA,KAAK,YAAY;AACf,UAAI,CAAC,MAAM,QAAQ,GAAG,KAAK,IAAI,WAAW,EAAG,OAAM,IAAI,MAAM,kCAAkC,KAAK,kCAAkC;AACtI,aAAO,GAAG,GAAG,YAAY,KAAK,QAAQ,IAAI,CAAC,CAAC,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,CAAC,CAAC;AAAA,IAC3E;AAAA,IACA,KAAK;AAAa,aAAO,GAAG,GAAG,SAAS,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,GAAG,CAAC;AAAA,IACxE,KAAK;AAAgB,aAAO,GAAG,GAAG,aAAa,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,GAAG,CAAC;AAAA,IAC/E,KAAK;AAAe,aAAO,GAAG,GAAG,SAAS,KAAK,QAAQ,GAAG,OAAO,GAAG,CAAC,GAAG,CAAC;AAAA,IACzE,KAAK;AAAa,aAAO,GAAG,GAAG,SAAS,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;AAAA,IACvE,KAAK;AAAS,aAAO,MAAM,GAAG,GAAG,aAAa,GAAG,GAAG;AAAA,IACpD,KAAK;AAAW,aAAO,MAAM,GAAG,GAAG,iBAAiB,GAAG,GAAG;AAAA,IAC1D;AACE,YAAM,IAAI,MAAM,0CAA0C,EAAE,SAAS,KAAK,kBAAkB;AAAA,EAChG;AACF;;;AChIA,SAAS,0BAA0B;AAiBnC,IAAM,gBAAyD;AAAA,EAC7D,SAAS,MAAM;AAAA,EACf,OAAO,CAAC,QAAQ,OAAO,GAAG;AAAA,EAC1B,OAAO,CAAC,QAAQ,OAAO,GAAG;AAAA,EAC1B,OAAO,CAAC,QAAQ,OAAO,GAAG;AAAA,EAC1B,OAAO,CAAC,QAAQ,OAAO,GAAG;AAAA,EAC1B,kBAAkB,CAAC,QAAQ,kBAAkB,GAAG;AAClD;AAGO,IAAM,+BAA+B,OAAO,KAAK,aAAa;AAkB9D,IAAM,0BAA0B,oBAAI,IAAI,CAAC,UAAU,UAAU,SAAS,CAAC;AAQ9E,IAAM,kBAAkB;AAejB,IAAM,oBAAN,MAAqD;AAAA,EAArD;AACL,SAAS,OAAO;AAChB,SAAS,WAAW;AAAA;AAAA,EAEpB,UAAU,OAAuB,KAA+B;AAC9D,QAAI,CAAC,MAAM,KAAM,QAAO;AASxB,QAAI,MAAM,gBAAgB,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,WAAW,EAAG,QAAO;AAUjE,QAAI,OAAO,IAAI,qBAAqB,YAAY;AAC9C,YAAM,OAAO,IAAI,QAAQ,MAAM,IAAI;AACnC,UAAI,MAAM;AACR,YAAI,IAAI,iBAAiB,KAAK,kBAAkB,IAAI,CAAC,EAAG,QAAO;AAC/D,cAAM,cAAc,KAAK,QAAQ,OAAO,OAAO,KAAK,KAAK,IAAI,CAAC;AAC9D,mBAAW,KAAK,aAAa;AAC3B,gBAAM,eAAgB,GAAyB;AAC/C,cAAI,gBAAgB,IAAI,iBAAiB,YAAY,EAAG,QAAO;AAAA,QACjE;AAAA,MACF;AAAA,IACF;AACA,UAAM,OAAO,IAAI,kBAAkB,MAAM,IAAI;AAC7C,WAAO,KAAK,aAAa,OAAO,IAAI,kBAAkB;AAAA,EACxD;AAAA,EAEA,MAAM,QAAQ,OAAuB,KAAgD;AACnF,UAAM,EAAE,KAAK,OAAO,IAAI,MAAM,KAAK,YAAY,OAAO,GAAG;AACzD,UAAM,OAAO,IAAI,QAAQ,MAAM,IAAK;AACpC,UAAM,aAAa,KAAK,kBAAkB,IAAI;AAE9C,UAAM,OAAO,MAAM,IAAI,cAAe,YAAY,KAAK,MAAM;AAG7D,UAAM,SAAS,KAAK,eAAe,OAAO,IAAI;AAE9C,WAAO,EAAE,MAAM,QAAQ,IAAI;AAAA,EAC7B;AAAA,EAEA,MAAM,YAAY,OAAuB,KAAmE;AAC1G,UAAM,OAAO,IAAI,QAAQ,MAAM,IAAK;AACpC,QAAI,CAAC,MAAM;AACT,YAAM,IAAI,MAAM,mBAAmB,MAAM,IAAI,EAAE;AAAA,IACjD;AAEA,UAAM,SAAoB,CAAC;AAC3B,UAAM,gBAA0B,CAAC;AACjC,UAAM,iBAA2B,CAAC;AAClC,UAAM,YAAY,KAAK,kBAAkB,IAAI;AAG7C,UAAM,QAAQ,oBAAI,IAAoB;AAGtC,QAAI,MAAM,cAAc,MAAM,WAAW,SAAS,GAAG;AACnD,iBAAW,OAAO,MAAM,YAAY;AAClC,cAAM,UAAU,KAAK,oBAAoB,MAAM,KAAK,WAAW,KAAK;AACpE,sBAAc,KAAK,GAAG,OAAO,QAAQ,GAAG,GAAG;AAC3C,uBAAe,KAAK,OAAO;AAAA,MAC7B;AAAA,IACF;AAGA,QAAI,MAAM,YAAY,MAAM,SAAS,SAAS,GAAG;AAC/C,iBAAW,WAAW,MAAM,UAAU;AACpC,cAAM,UAAU,KAAK,kBAAkB,MAAM,SAAS,WAAW,KAAK;AACtE,sBAAc,KAAK,GAAG,OAAO,QAAQ,OAAO,GAAG;AAAA,MACjD;AAAA,IACF;AAKA,UAAM,eAAyB,CAAC;AAChC,UAAM,YAAY,KAAK;AAAA,MACrB,6BAA6B,KAAK;AAAA,MAClC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,QAAI,UAAW,cAAa,KAAK,SAAS;AAG1C,QAAI,MAAM,kBAAkB,MAAM,eAAe,SAAS,GAAG;AAC3D,iBAAW,MAAM,MAAM,gBAAgB;AACrC,cAAM,UAAU,KAAK,gBAAgB,MAAM,GAAG,WAAW,WAAW,KAAK;AACzE,YAAI,GAAG,WAAW;AAChB,gBAAM,QAAQ,MAAM,QAAQ,GAAG,SAAS,IAAI,GAAG,YAAY,CAAC,GAAG,WAAW,GAAG,SAAS;AACtF,cAAI,MAAM,WAAW,GAAG;AAOtB,kBAAM,MAAM,KAAK,qBAAqB,MAAM,GAAG,WAAW,SAAS;AACnE,kBAAM,SAAS,KAAK,eAAe,KAAK,KAAK,OAAO;AAQpD,kBAAM,UAAU,mBAAmB,MAAM,CAAC,CAAC;AAC3C,mBAAO,KAAK,KAAK,eAAe,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC;AACnD,kBAAM,QAAQ,GAAG,MAAM,QAAQ,OAAO,MAAM;AAC5C,gBAAI,WAAW,MAAM;AACnB,qBAAO,KAAK,KAAK,eAAe,KAAK,KAAK,OAAO,CAAC;AAClD,2BAAa,KAAK,IAAI,KAAK,QAAQ,MAAM,OAAO,OAAO,MAAM,GAAG;AAAA,YAClE,OAAO;AACL,qBAAO,KAAK,KAAK,eAAe,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC;AACnD,2BAAa,KAAK,IAAI,KAAK,QAAQ,MAAM,QAAQ,OAAO,MAAM,GAAG;AAAA,YACnE;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAIA,UAAM,UAAU,IAAI,0BAA0B,MAAM,IAAK;AACzD,QAAI,SAAS;AACX,iBAAW,SAAS,MAAM,KAAK,GAAG;AAChC,YAAI,CAAC,QAAQ,IAAI,KAAK,GAAG;AACvB,gBAAM,IAAI;AAAA,YACR,6BAA6B,KAAK,uDACzB,MAAM,IAAI;AAAA,UACrB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,SAAK,eAAe,KAAK,kBAAkB,IAAI,GAAG,WAAW,KAAK,cAAc,MAAM;AACtF,eAAW,SAAS,MAAM,KAAK,GAAG;AAIhC,YAAM,eAAe,KAAK,QAAQ,KAAK,GAAG,QAAQ;AAClD,WAAK,eAAe,cAAc,OAAO,KAAK,cAAc,MAAM;AAAA,IACpE;AAEA,QAAI,MAAM,UAAU,cAAc,KAAK,IAAI,CAAC,UAAU,SAAS;AAC/D,QAAI,MAAM,OAAO,GAAG;AAClB,aAAO,MAAM,MAAM,KAAK,MAAM,OAAO,CAAC,EAAE,KAAK,GAAG;AAAA,IAClD;AACA,QAAI,aAAa,SAAS,GAAG;AAC3B,aAAO,UAAU,aAAa,KAAK,OAAO,CAAC;AAAA,IAC7C;AACA,QAAI,eAAe,SAAS,GAAG;AAC7B,aAAO,aAAa,eAAe,KAAK,IAAI,CAAC;AAAA,IAC/C;AACA,QAAI,MAAM,SAAS,OAAO,KAAK,MAAM,KAAK,EAAE,SAAS,GAAG;AACtD,YAAM,eAAe,OAAO,QAAQ,MAAM,KAAK,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,KAAK,EAAE,YAAY,CAAC,EAAE;AAC5F,aAAO,aAAa,aAAa,KAAK,IAAI,CAAC;AAAA,IAC7C;AACA,QAAI,MAAM,SAAS,MAAM;AACvB,aAAO,UAAU,MAAM,KAAK;AAAA,IAC9B;AACA,QAAI,MAAM,UAAU,MAAM;AACxB,aAAO,WAAW,MAAM,MAAM;AAAA,IAChC;AAEA,WAAO,EAAE,KAAK,OAAO;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaQ,eACN,YACA,OACA,KACA,cACA,QACM;AACN,QAAI,OAAO,IAAI,iBAAiB,WAAY;AAC5C,UAAM,SAAS,IAAI,aAAa,UAAU;AAC1C,QAAI,WAAW,UAAa,WAAW,KAAM;AAC7C,UAAM,EAAE,KAAK,QAAQ,YAAY,IAAI,yBAAyB,QAAQ,KAAK;AAC3E,QAAI,CAAC,IAAK;AACV,QAAI,IAAI;AACR,UAAM,WAAW,IAAI,QAAQ,OAAO,MAAM;AACxC,aAAO,KAAK,YAAY,GAAG,CAAC;AAC5B,aAAO,IAAI,OAAO,MAAM;AAAA,IAC1B,CAAC;AACD,iBAAa,KAAK,IAAI,QAAQ,GAAG;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA,EAKQ,UAAU,MAAsB;AACtC,WAAO,KAAK,QAAQ,OAAO,IAAI;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAqBQ,uBACN,QACA,aACA,OACA,MACQ;AACR,QAAI,CAAC,OAAO,SAAS,GAAG,GAAG;AAOzB,YAAM,UAAU,CAAC,CAAC,MAAM,SAAS,OAAO,KAAK,KAAK,KAAK,EAAE,SAAS;AAClE,UAAI,WAAW,2BAA2B,KAAK,MAAM,GAAG;AACtD,eAAO,IAAI,WAAW,MAAM,MAAM;AAAA,MACpC;AACA,aAAO;AAAA,IACT;AAOA,QAAI,CAAC,gBAAgB,KAAK,MAAM,EAAG,QAAO;AAM1C,UAAM,WAAW,OAAO,MAAM,GAAG;AACjC,UAAM,SAAS,SAAS,SAAS,SAAS,CAAC;AAC3C,UAAM,OAAO,SAAS,MAAM,GAAG,EAAE;AACjC,QAAI,KAAK,WAAW,KAAK,CAAC,OAAQ,QAAO;AACzC,QAAI,cAAc;AAClB,QAAI,SAAS;AACb,eAAW,OAAO,MAAM;AACtB,eAAS,SAAS,GAAG,MAAM,IAAI,GAAG,KAAK;AACvC,YAAM,QAAQ,KAAK,UAAU,MAAM;AACnC,UAAI,CAAC,MAAM,IAAI,KAAK,GAAG;AAIrB,cAAM,YAAY,MAAM,QAAQ,KAAK,GAAG,QAAQ;AAGhD,cAAM,WAAW,cAAc,QAAQ,IAAI,KAAK,MAAM,IAAI,SAAS,MAAM,KAAK;AAC9E,cAAM;AAAA,UACJ;AAAA,UACA,aAAa,QAAQ,QAAQ,WAAW,MAAM,GAAG,QAAQ,KAAK;AAAA,QAChE;AAAA,MACF;AACA,oBAAc;AAAA,IAChB;AACA,WAAO,IAAI,WAAW,MAAM,MAAM;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeQ,aACN,MACA,QACA,MAC4C;AAC5C,UAAM,MAAM,SAAS,cAAc,KAAK,aAAa,KAAK;AAE1D,QAAI,IAAI,MAAM,EAAG,QAAO,IAAI,MAAM;AAClC,QAAI,OAAO,SAAS,GAAG,GAAG;AACxB,YAAM,CAAC,OAAO,GAAG,IAAI,IAAI,OAAO,MAAM,GAAG;AACzC,YAAM,OAAO,KAAK,KAAK,GAAG;AAE1B,UAAI,UAAU,KAAK,QAAQ,IAAI,IAAI,EAAG,QAAO,IAAI,IAAI;AAErD,UAAI,IAAI,IAAI,EAAG,QAAO,IAAI,IAAI;AAE9B,YAAM,OAAO,OAAO,QAAQ,OAAO,GAAG;AACtC,UAAI,IAAI,IAAI,EAAG,QAAO,IAAI,IAAI;AAE9B,UAAI,SAAS,aAAa;AACxB,eAAO,EAAE,KAAK,QAAQ,MAAM,SAAS;AAAA,MACvC;AAAA,IACF,WAAW,IAAI,MAAM,GAAG;AACtB,aAAO,IAAI,MAAM;AAAA,IACnB;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,oBACN,MACA,QACA,aACA,OACQ;AACR,UAAM,MAAM,KAAK,aAAa,MAAM,QAAQ,WAAW;AACvD,UAAM,MAAM,MAAM,IAAI,MAAO,OAAO,SAAS,GAAG,IAAI,OAAO,MAAM,GAAG,EAAE,CAAC,IAAI;AAC3E,WAAO,KAAK,uBAAuB,KAAK,aAAa,OAAO,IAAI;AAAA,EAClE;AAAA,EAEQ,kBACN,MACA,QACA,aACA,OACQ;AACR,UAAM,UAAU,KAAK,aAAa,MAAM,QAAQ,SAAS;AAOzD,QAAI,CAAC,SAAS;AACZ,YAAM,WAAW,OAAO,KAAK,KAAK,YAAY,CAAC,CAAC;AAChD,YAAM,IAAI;AAAA,QACR,+BAA+B,KAAK,IAAI,0BAA0B,MAAM,OACrE,SAAS,SAAS,eAAe,SAAS,KAAK,IAAI,CAAC,MAAM;AAAA,MAC/D;AAAA,IACF;AAEA,UAAM,MAAM,QAAQ,QAAQ,MACxB,MACA,KAAK,uBAAuB,QAAQ,KAAK,aAAa,OAAO,IAAI;AAErE,UAAM,OAAO,cAAc,QAAQ,IAAI;AACvC,QAAI,KAAM,QAAO,KAAK,GAAG;AAMzB,QAAI,wBAAwB,IAAI,QAAQ,IAAI,EAAG,QAAO;AAEtD,UAAM,IAAI;AAAA,MACR,kCAAkC,MAAM,cAAc,KAAK,IAAI,4BACvC,QAAQ,IAAI,mCAC9B,6BAA6B,KAAK,IAAI,CAAC,kCACvC,CAAC,GAAG,uBAAuB,EAAE,KAAK,IAAI,CAAC;AAAA,IAC/C;AAAA,EACF;AAAA,EAEQ,gBACN,MACA,QACA,aACA,OACQ;AACR,UAAM,MAAM,KAAK,aAAa,MAAM,QAAQ,WAAW;AACvD,QAAI,IAAK,QAAO,KAAK,uBAAuB,IAAI,KAAK,aAAa,OAAO,IAAI;AAC7E,UAAM,UAAU,KAAK,aAAa,MAAM,QAAQ,SAAS;AACzD,QAAI,QAAS,QAAO,KAAK,uBAAuB,QAAQ,KAAK,aAAa,OAAO,IAAI;AACrF,UAAM,YAAY,OAAO,SAAS,GAAG,IAAI,OAAO,MAAM,GAAG,EAAE,CAAC,IAAI;AAChE,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBQ,qBACN,MACA,QACA,WACmC;AACnC,UAAM,MAAM,KAAK,aAAa,MAAM,QAAQ,WAAW;AACvD,UAAM,UAAU,MAAM,SAAY,KAAK,aAAa,MAAM,QAAQ,SAAS;AAC3E,UAAM,SAAS,KAAK,OAAO,SAAS,QAAQ,OAAO,SAAS,GAAG,IAAI,OAAO,MAAM,GAAG,EAAE,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI;AAE1G,QAAI,OAAO,SAAS,GAAG,GAAG;AAGxB,YAAM,WAAW,OAAO,MAAM,GAAG;AACjC,YAAM,QAAQ,SAAS,SAAS,SAAS,CAAC;AAC1C,YAAM,UAAU,SAAS,MAAM,GAAG,EAAE,EAAE,KAAK,GAAG;AAC9C,YAAM,SAAS,KAAK,QAAQ,KAAK,UAAU,OAAO,CAAC,GAAG,QAAQ;AAC9D,aAAO,EAAE,QAAQ,MAAM;AAAA,IACzB;AACA,WAAO,EAAE,QAAQ,WAAW,OAAO,OAAO;AAAA,EAC5C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWQ,eACN,KACA,QACA,OACS;AACT,QAAI,OAAO,IAAI,8BAA8B,YAAY;AACvD,YAAM,UAAU,IAAI,0BAA0B,OAAO,QAAQ,OAAO,OAAO,KAAK;AAGhF,UAAI,YAAY,MAAO,QAAO;AAAA,IAChC;AACA,WAAO,wBAAwB,KAAK;AAAA,EACtC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaQ,eACN,KACA,QACA,KACQ;AACR,QAAI,OAAO,IAAI,+BAA+B,WAAY,QAAO;AACjE,WAAO,IAAI,2BAA2B,OAAO,QAAQ,OAAO,OAAO,GAAG,KAAK;AAAA,EAC7E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBQ,kBACN,MACA,MACA,aACA,OACA,QACA,KACe;AACf,QAAI,CAAC,KAAM,QAAO;AAElB,QAAI,KAAK,SAAS,QAAQ;AACxB,YAAM,UAAU,KAAK,gBAAgB,MAAM,KAAK,QAAQ,aAAa,KAAK;AAG1E,YAAM,SAAS,KAAK,qBAAqB,MAAM,KAAK,QAAQ,WAAW;AACvE,aAAO,KAAK,kBAAkB,SAAS,KAAK,UAAU,KAAK,QAAQ,QAAQ,KAAK,MAAM;AAAA,IACxF;AAEA,QAAI,KAAK,SAAS,OAAO;AACvB,YAAM,QAAQ,KAAK,kBAAkB,KAAK,OAAO,MAAM,aAAa,OAAO,QAAQ,GAAG;AACtF,aAAO,QAAQ,QAAQ,KAAK,MAAM;AAAA,IACpC;AAEA,UAAM,QAAQ,KAAK,SAChB,IAAI,CAAC,UAAU,KAAK,kBAAkB,OAAO,MAAM,aAAa,OAAO,QAAQ,GAAG,CAAC,EACnF,OAAO,CAAC,MAAmB,CAAC,CAAC,CAAC;AACjC,QAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAI,MAAM,WAAW,EAAG,QAAO,MAAM,CAAC;AACtC,WAAO,IAAI,MAAM,KAAK,KAAK,SAAS,OAAO,SAAS,OAAO,CAAC;AAAA,EAC9D;AAAA,EAEQ,kBACN,QACA,UACA,QACA,QACA,KACA,QACe;AACf,UAAM,QAAgC;AAAA,MACpC,QAAQ;AAAA,MAAK,WAAW;AAAA,MAAM,IAAI;AAAA,MAAK,KAAK;AAAA,MAAM,IAAI;AAAA,MAAK,KAAK;AAAA,MAChE,UAAU;AAAA,MAAQ,aAAa;AAAA,MAC/B,YAAY;AAAA,MAAQ,UAAU;AAAA,IAChC;AAEA,UAAM,cAAqD;AAAA,MACzD,UAAU,CAAC,MAAM,IAAI,CAAC;AAAA,MACtB,aAAa,CAAC,MAAM,IAAI,CAAC;AAAA,MACzB,YAAY,CAAC,MAAM,GAAG,CAAC;AAAA,MACvB,UAAU,CAAC,MAAM,IAAI,CAAC;AAAA,IACxB;AAKA,QAAI,aAAa,MAAO,QAAO,GAAG,MAAM;AACxC,QAAI,aAAa,SAAU,QAAO,GAAG,MAAM;AAE3C,QAAI,aAAa,QAAQ,aAAa,SAAS;AAC7C,UAAI,CAAC,UAAU,OAAO,WAAW,EAAG,QAAO;AAI3C,YAAM,eAAe,OAAO,IAAI,OAAK;AAAE,eAAO,KAAK,KAAK,eAAe,KAAK,QAAQ,CAAC,CAAC;AAAG,eAAO,IAAI,OAAO,MAAM;AAAA,MAAI,CAAC,EAAE,KAAK,IAAI;AACjI,aAAO,GAAG,KAAK,eAAe,KAAK,QAAQ,MAAM,CAAC,IAAI,aAAa,OAAO,OAAO,QAAQ,KAAK,YAAY;AAAA,IAC5G;AAEA,UAAM,QAAQ,MAAM,QAAQ;AAC5B,QAAI,CAAC,SAAS,CAAC,UAAU,OAAO,WAAW,EAAG,QAAO;AAIrD,UAAM,UAAU,YAAY,QAAQ;AACpC,QAAI,SAAS;AACX,aAAO,KAAK,QAAQ,OAAO,CAAC,CAAC,CAAC;AAC9B,aAAO,GAAG,MAAM,IAAI,KAAK,KAAK,OAAO,MAAM;AAAA,IAC7C;AAKA,QAAI,aAAa,OAAO;AACtB,YAAM,UAAU,mBAAmB,OAAO,CAAC,CAAC;AAC5C,UAAI,WAAW,MAAM;AACnB,eAAO,KAAK,KAAK,eAAe,KAAK,QAAQ,OAAO,CAAC;AACrD,eAAO,GAAG,KAAK,eAAe,KAAK,QAAQ,MAAM,CAAC,OAAO,OAAO,MAAM;AAAA,MACxE;AAAA,IACF;AAQA,WAAO,KAAK,KAAK,eAAe,KAAK,QAAQ,OAAO,CAAC,CAAC,CAAC;AACvD,WAAO,GAAG,KAAK,eAAe,KAAK,QAAQ,MAAM,CAAC,IAAI,KAAK,KAAK,OAAO,MAAM;AAAA,EAC/E;AAAA,EAEQ,kBAAkB,MAAoB;AAC5C,WAAO,KAAK,IAAI,KAAK;AAAA,EACvB;AAAA,EAEQ,eAAe,OAAuB,MAAmD;AAC/F,UAAM,SAAgD,CAAC;AACvD,QAAI,MAAM,YAAY;AACpB,iBAAW,OAAO,MAAM,YAAY;AAClC,cAAM,IAAI,KAAK,aAAa,MAAM,KAAK,WAAW;AAClD,eAAO,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,QAAQ,SAAS,CAAC;AAAA,MACtD;AAAA,IACF;AACA,QAAI,MAAM,UAAU;AAClB,iBAAW,KAAK,MAAM,UAAU;AAC9B,eAAO,KAAK,EAAE,MAAM,GAAG,MAAM,SAAS,CAAC;AAAA,MACzC;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACF;;;AC3qBA,SAAS,sBAAAC,2BAA0B;;;ACiB5B,IAAM,uBAA4C,oBAAI,IAAwB;AAAA,EACnF;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGM,IAAM,oBAAoB;AAuBjC,SAAS,eAAe,GAAoB;AAC1C,MAAI,KAAK,KAAM,QAAO;AACtB,MAAI,OAAO,MAAM,SAAU,QAAO;AAClC,MAAI,aAAa,KAAM,QAAO,EAAE,QAAQ;AACxC,QAAM,IAAI,OAAO,CAAC;AAClB,MAAI,OAAO,SAAS,CAAC,EAAG,QAAO;AAC/B,SAAO,KAAK,MAAM,OAAO,CAAC,CAAC;AAC7B;AAaA,SAAS,UAAU,QAA4B,KAAc,MAAwB;AACnF,MAAI,WAAW,SAAS,WAAW,OAAO;AACxC,QAAI,QAAQ,OAAW,QAAO,QAAQ;AACtC,UAAM,IAAI,eAAe,GAAG;AAC5B,UAAMC,KAAI,eAAe,IAAI;AAC7B,QAAI,OAAO,MAAMA,EAAC,EAAG,QAAO;AAC5B,QAAI,OAAO,MAAM,CAAC,EAAG,QAAO;AAC5B,UAAM,WAAW,WAAW,QAAQA,KAAI,IAAIA,KAAI;AAChD,WAAO,WAAW,OAAO;AAAA,EAC3B;AACA,QAAM,IAAI,OAAO,QAAQ,CAAC;AAC1B,SAAO,QAAQ,SAAY,IAAI,OAAO,GAAG,IAAI;AAC/C;AAYO,SAAS,oBACd,UACA,eACA,WACA,UAC2B;AAC3B,QAAM,UAAU,oBAAI,IAAqC;AAEzD,aAAW,OAAO,UAAU;AAE1B,UAAM,WAAoC,CAAC;AAC3C,eAAW,MAAM,WAAW;AAC1B,YAAM,KAAK,IAAI,GAAG,OAAO;AACzB,eAAS,GAAG,UAAU,IAAI,GAAG,SAAS,IAAI,EAAE,IAAI,GAAG,SAAS,IAAI,EAAE,IAAI;AAAA,IACxE;AAIA,UAAM,WAAqB,CAAC;AAM5B,eAAW,KAAK,cAAe,UAAS,KAAK,GAAG,CAAC,IAAI,KAAK,UAAU,IAAI,CAAC,KAAK,IAAI,CAAC,EAAE;AACrF,eAAW,MAAM,UAAW,UAAS,KAAK,GAAG,GAAG,UAAU,IAAI,OAAO,SAAS,GAAG,UAAU,CAAC,CAAC,EAAE;AAC/F,UAAM,MAAM,SAAS,KAAK,GAAG;AAE7B,QAAI,SAAS,QAAQ,IAAI,GAAG;AAC5B,QAAI,CAAC,QAAQ;AACX,eAAS,CAAC;AACV,iBAAW,KAAK,cAAe,QAAO,CAAC,IAAI,IAAI,CAAC;AAChD,iBAAW,MAAM,UAAW,QAAO,GAAG,UAAU,IAAI,SAAS,GAAG,UAAU;AAC1E,cAAQ,IAAI,KAAK,MAAM;AAAA,IACzB;AACA,eAAW,KAAK,UAAU;AACxB,aAAO,EAAE,KAAK,IAAI,UAAU,EAAE,QAAQ,OAAO,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,CAAC;AAAA,IACrE;AAAA,EACF;AAEA,SAAO,CAAC,GAAG,QAAQ,OAAO,CAAC;AAC7B;;;AD3HA,IAAM,iBAAyC;AAAA,EAC7C,QAAQ;AAAA,EAAK,WAAW;AAAA,EAAM,IAAI;AAAA,EAAK,KAAK;AAAA,EAAM,IAAI;AAAA,EAAK,KAAK;AAClE;AAyBO,IAAM,mBAAN,MAAoD;AAAA,EAApD;AACL,SAAS,OAAO;AAChB,SAAS,WAAW;AAAA;AAAA,EAEpB,UAAU,OAAuB,KAA+B;AAC9D,QAAI,CAAC,MAAM,KAAM,QAAO;AACxB,UAAM,OAAO,IAAI,kBAAkB,MAAM,IAAI;AAC7C,WAAO,KAAK,qBAAqB,OAAO,IAAI,qBAAqB;AAAA,EACnE;AAAA,EAEA,MAAM,QAAQ,OAAuB,KAAgD;AACnF,UAAM,OAAO,IAAI,QAAQ,MAAM,IAAK;AACpC,UAAM,aAAa,KAAK,kBAAkB,IAAI;AAS9C,UAAM,YAAY,oBAAI,IAAoB;AAC1C,eAAW,MAAM,MAAM,kBAAkB,CAAC,GAAG;AAC3C,UAAI,GAAG,YAAa,WAAU,IAAI,GAAG,WAAW,GAAG,WAAW;AAAA,IAChE;AACA,UAAM,UAAyB,CAAC;AAChC,QAAI,MAAM,cAAc,MAAM,WAAW,SAAS,GAAG;AACnD,iBAAW,OAAO,MAAM,YAAY;AAClC,cAAM,QAAQ,KAAK,iBAAiB,MAAM,KAAK,WAAW;AAC1D,cAAM,OAAO,UAAU,IAAI,GAAG;AAC9B,gBAAQ,KAAK,OAAO,EAAE,OAAO,iBAAiB,KAAK,IAAI,KAAK;AAC5D,kBAAU,OAAO,GAAG;AAAA,MACtB;AAAA,IACF;AAEA,eAAW,CAAC,KAAK,IAAI,KAAK,WAAW;AACnC,cAAQ,KAAK,EAAE,OAAO,KAAK,iBAAiB,MAAM,KAAK,WAAW,GAAG,iBAAiB,KAAK,CAAC;AAAA,IAC9F;AAGA,UAAM,eAAwE,CAAC;AAC/E,QAAI,MAAM,YAAY,MAAM,SAAS,SAAS,GAAG;AAC/C,iBAAW,WAAW,MAAM,UAAU;AACpC,cAAM,EAAE,OAAO,OAAO,IAAI,KAAK,0BAA0B,MAAM,OAAO;AACtE,qBAAa,KAAK,EAAE,OAAO,QAAQ,OAAO,QAAQ,CAAC;AAAA,MACrD;AAAA,IACF;AAOA,UAAM,SAAkC,CAAC;AAGzC,UAAM,YAAuC,CAAC;AAC9C,SAAK,gBAAgB,6BAA6B,KAAK,GAAG,MAAM,QAAQ,SAAS;AAIjF,eAAW,EAAE,OAAO,OAAO,KAAK,KAAK,gBAAgB,MAAM,KAAK,GAAG;AACjE,YAAM,QAAQ,KAAK,mBAAmB,QAAQ,OAAO,MAAM;AAC3D,UAAI,MAAO,WAAU,KAAK,KAAK;AAAA,IACjC;AACA,QAAI,UAAU,SAAS,GAAG;AACxB,aAAO,OAAO,CAAC,GAAI,MAAM,QAAQ,OAAO,IAAI,IAAI,OAAO,OAAO,CAAC,GAAI,GAAG,SAAS;AAAA,IACjF;AAQA,UAAM,OAAO,KAAK,gBAAgB,MAAM,OAAO,MAAM;AACrD,QAAI,MAAM;AACR,aAAO,KAAK,mBAAmB,MAAM,OAAO,cAAc,QAAQ,MAAM,GAAG;AAAA,IAC7E;AAKA,UAAM,OAAO,MAAM,IAAI,iBAAkB,YAAY;AAAA;AAAA;AAAA;AAAA,MAInD,SAAS,QAAQ,SAAS,IAAK,UAAkC;AAAA,MACjE,cAAc,aAAa,SAAS,IAAI,eAAe;AAAA,MACvD,QAAQ,KAAK,cAAc,YAAY,QAAQ,GAAG;AAAA;AAAA;AAAA;AAAA,MAIlD,UAAU,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAMhB,SAAS,IAAI;AAAA,IACf,CAAC;AAMD,UAAM,aAAa,KAAK,IAAI,SAAO;AACjC,YAAM,SAAkC,CAAC;AACzC,iBAAW,OAAO,KAAK,oBAAoB,KAAK,GAAG;AACjD,cAAM,YAAY,KAAK,iBAAiB,MAAM,KAAK,WAAW;AAC9D,YAAI,aAAa,IAAK,QAAO,GAAG,IAAI,IAAI,SAAS;AAAA,MACnD;AACA,UAAI,MAAM,UAAU;AAClB,mBAAW,KAAK,MAAM,UAAU;AAE9B,cAAI,KAAK,IAAK,QAAO,CAAC,IAAI,IAAI,CAAC;AAAA,QACjC;AAAA,MACF;AACA,aAAO;AAAA,IACT,CAAC;AAED,UAAM,SAAS,KAAK,eAAe,OAAO,IAAI;AAU9C,QAAI;AACJ,QAAI;AACF,aAAO,MAAM,KAAK,YAAY,OAAO,GAAG,GAAG;AAAA,IAC7C,QAAQ;AACN,YAAM;AAAA,IACR;AACA,WAAO,MAAM,EAAE,MAAM,YAAY,QAAQ,IAAI,IAAI,EAAE,MAAM,YAAY,OAAO;AAAA,EAC9E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBA,MAAM,YAAY,OAAuB,KAAmE;AAC1G,UAAM,OAAO,IAAI,QAAQ,MAAM,IAAK;AACpC,QAAI,CAAC,MAAM;AACT,YAAM,IAAI,MAAM,mBAAmB,MAAM,IAAI,EAAE;AAAA,IACjD;AAEA,UAAM,cAAwB,CAAC;AAC/B,UAAM,eAAyB,CAAC;AAChC,UAAM,SAAoB,CAAC;AAK3B,UAAM,YAAY,oBAAI,IAAoB;AAC1C,eAAW,MAAM,MAAM,kBAAkB,CAAC,GAAG;AAC3C,UAAI,GAAG,YAAa,WAAU,IAAI,GAAG,WAAW,GAAG,WAAW;AAAA,IAChE;AACA,UAAM,YAAY,KAAK,kBAAkB,IAAI;AAQ7C,UAAM,OAAO,KAAK,gBAAgB,MAAM,OAAO,OAAO;AAAA,MACpD,oBAAoB,6BAA6B,KAAK,CAAC,EACpD,IAAI,CAAC,MAAM,CAAC,KAAK,iBAAiB,MAAM,EAAE,QAAQ,KAAK,GAAG,IAAI,CAAC;AAAA,IACpE,CAAC;AACD,UAAM,aAAa,IAAI,KAAK,MAAM,aAAa,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,YAAY,EAAE,CAAC,CAAC;AACnF,UAAM,cAAwB,CAAC;AAC/B,UAAM,UAAU,CAAC,QAAwB;AACvC,YAAM,KAAK,WAAW,IAAI,GAAG;AAC7B,UAAI,IAAI;AACN,oBAAY;AAAA,UACV,cAAc,GAAG,SAAS,SAAS,SAAS,MAAM,GAAG,OAAO,QAAQ,GAAG,SAAS;AAAA,QAClF;AACA,eAAO,IAAI,GAAG,SAAS,MAAM,GAAG,IAAI;AAAA,MACtC;AACA,YAAM,MAAM,KAAK,iBAAiB,MAAM,KAAK,WAAW;AACxD,YAAM,OAAO,UAAU,IAAI,GAAG;AAC9B,aAAO,OAAO,eAAe,IAAI,MAAM,GAAG,MAAM;AAAA,IAClD;AAEA,QAAI,MAAM,YAAY;AACpB,iBAAW,OAAO,MAAM,YAAY;AAClC,cAAM,OAAO,QAAQ,GAAG;AACxB,oBAAY,KAAK,GAAG,IAAI,QAAQ,GAAG,GAAG;AACtC,qBAAa,KAAK,IAAI;AAAA,MACxB;AAAA,IACF;AAGA,eAAW,CAAC,GAAG,KAAK,WAAW;AAC7B,UAAI,MAAM,YAAY,SAAS,GAAG,EAAG;AACrC,YAAM,OAAO,QAAQ,GAAG;AACxB,kBAAY,KAAK,GAAG,IAAI,QAAQ,GAAG,GAAG;AACtC,mBAAa,KAAK,IAAI;AAAA,IACxB;AACA,QAAI,MAAM,UAAU;AAClB,iBAAW,KAAK,MAAM,UAAU;AAC9B,cAAM,EAAE,OAAO,OAAO,IAAI,KAAK,0BAA0B,MAAM,CAAC;AAChE,cAAM,SAAS,WAAW,UACtB,aACA,WAAW,mBACT,kBAAkB,KAAK,MACvB,GAAG,OAAO,YAAY,CAAC,IAAI,KAAK;AACtC,oBAAY,KAAK,GAAG,MAAM,QAAQ,CAAC,GAAG;AAAA,MACxC;AAAA,IACF;AAsBA,UAAM,aAAuB,CAAC;AAK9B,UAAM,eAAe,KAAK;AAAA,MACxB,6BAA6B,KAAK;AAAA,MAClC;AAAA,MACA;AAAA,IACF;AACA,QAAI,aAAc,YAAW,KAAK,YAAY;AAO9C,eAAW,EAAE,OAAO,OAAO,KAAK,KAAK,gBAAgB,MAAM,KAAK,GAAG;AACjE,YAAM,UAAUC,oBAAmB,OAAO,IAAI;AAC9C,aAAO,KAAK,OAAO,MAAM,WAAW,OAAO,IAAI;AAC/C,iBAAW;AAAA,QACT,IAAI,KAAK,QAAQ,OAAO,SAAS,CAAC,QAAQ,KAAK,IAAI,UAAU,MAAM,IAAI,KAAK,OAAO,MAAM;AAAA,MAC3F;AAAA,IACF;AAKA,UAAM,QAAQ,IAAI,eAAe,SAAS;AAC1C,QAAI,SAAS,MAAM;AACjB,YAAM,EAAE,KAAK,UAAU,QAAQ,YAAY,IAAI,yBAAyB,OAAO,SAAS;AACxF,UAAI,UAAU;AACZ,YAAI,IAAI;AAER,cAAM,WAAW,SAAS,QAAQ,OAAO,MAAM;AAC7C,iBAAO,KAAK,YAAY,GAAG,CAAC;AAC5B,iBAAO,IAAI,OAAO,MAAM;AAAA,QAC1B,CAAC;AACD,mBAAW,KAAK,IAAI,QAAQ,GAAG;AAAA,MACjC;AAAA,IACF;AAEA,QAAI,MAAM,UAAU,YAAY,KAAK,IAAI,CAAC,UAAU,SAAS;AAC7D,QAAI,YAAY,SAAS,EAAG,QAAO,MAAM,YAAY,KAAK,GAAG;AAC7D,QAAI,WAAW,SAAS,GAAG;AACzB,aAAO,UAAU,WAAW,KAAK,OAAO,CAAC;AAAA,IAC3C;AACA,QAAI,aAAa,SAAS,GAAG;AAC3B,aAAO,aAAa,aAAa,KAAK,IAAI,CAAC;AAAA,IAC7C;AACA,QAAI,MAAM,SAAS,OAAO,KAAK,MAAM,KAAK,EAAE,SAAS,GAAG;AACtD,YAAM,eAAe,OAAO,QAAQ,MAAM,KAAK,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,KAAK,EAAE,YAAY,CAAC,EAAE;AAC5F,aAAO,aAAa,aAAa,KAAK,IAAI,CAAC;AAAA,IAC7C;AACA,QAAI,MAAM,SAAS,KAAM,QAAO,UAAU,MAAM,KAAK;AACrD,QAAI,MAAM,UAAU,KAAM,QAAO,WAAW,MAAM,MAAM;AAExD,WAAO,EAAE,KAAK,OAAO;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBQ,cACN,YACA,QACA,KACqC;AACrC,UAAM,aAAa,OAAO,KAAK,MAAM,EAAE,SAAS,IAAI,SAAS;AAC7D,QAAI,OAAO,IAAI,iBAAiB,WAAY,QAAO;AACnD,UAAM,QAAQ,IAAI,aAAa,UAAU;AACzC,QAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAClD,UAAM,cAAc;AACpB,QAAI,CAAC,WAAY,QAAO;AACxB,WAAO,EAAE,MAAM,CAAC,YAAY,WAAW,EAAE;AAAA,EAC3C;AAAA;AAAA,EAGQ,mBAAmB,MAAY,OAAe,YAA6B;AACjF,QAAI,CAAC,MAAM,SAAS,GAAG,EAAG,QAAO;AACjC,UAAM,QAAQ,MAAM,MAAM,GAAG,EAAE,CAAC;AAChC,UAAM,eAAe,KAAK,QAAQ,KAAK,GAAG,QAAQ;AAClD,WAAO,iBAAiB;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoBQ,gBACN,MACA,OACA,QACwB;AACxB,UAAM,aAAa,KAAK,kBAAkB,IAAI;AAM9C,eAAW,MAAM,MAAM,kBAAkB,CAAC,GAAG;AAC3C,YAAM,QAAQ,KAAK,iBAAiB,MAAM,GAAG,WAAW,WAAW;AACnE,UAAI,KAAK,mBAAmB,MAAM,OAAO,UAAU,GAAG;AACpD,cAAM,IAAI;AAAA,UACR,8EAA8E,KAAK;AAAA,QACrF;AAAA,MACF;AAAA,IACF;AAGA,UAAM,SAAS;AAAA,MACb,IAAI,MAAM,YAAY,CAAC,GAAG,IAAI,CAAC,OAAO,EAAE,OAAO,WAAW,OAAO,KAAK,0BAA0B,MAAM,CAAC,EAAE,MAAM,EAAE;AAAA,MACjH,GAAG,OAAO,KAAK,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,UAAU,OAAO,EAAE,EAAE;AAAA,IACnE,EAAE,OAAO,CAAC,MAAM,KAAK,mBAAmB,MAAM,EAAE,OAAO,UAAU,CAAC;AAClE,QAAI,OAAO,SAAS,GAAG;AACrB,YAAM,IAAI;AAAA,QACR,+DAA+D,OAAO,CAAC,EAAE,KAAK,MACzE,OAAO,CAAC,EAAE,KAAK,uHACwC,OAAO,CAAC,EAAE,KAAK;AAAA,MAC7E;AAAA,IACF;AAGA,UAAM,YAAkC,CAAC;AACzC,eAAW,OAAO,MAAM,cAAc,CAAC,GAAG;AACxC,YAAM,QAAQ,KAAK,iBAAiB,MAAM,KAAK,WAAW;AAC1D,UAAI,CAAC,KAAK,mBAAmB,MAAM,OAAO,UAAU,EAAG;AACvD,YAAM,CAAC,OAAO,GAAG,IAAI,IAAI,MAAM,MAAM,GAAG;AACxC,YAAM,OAAO,KAAK,KAAK,GAAG;AAC1B,UAAI,KAAK,SAAS,GAAG,GAAG;AACtB,cAAM,IAAI;AAAA,UACR,mFACgB,KAAK;AAAA,QACvB;AAAA,MACF;AACA,gBAAU,KAAK,EAAE,YAAY,KAAK,SAAS,OAAO,MAAM,WAAW,KAAK,QAAQ,KAAK,GAAG,QAAQ,MAAM,CAAC;AAAA,IACzG;AAEA,QAAI,UAAU,WAAW,EAAG,QAAO;AAGnC,eAAW,KAAK,MAAM,YAAY,CAAC,GAAG;AACpC,YAAM,EAAE,OAAO,IAAI,KAAK,0BAA0B,MAAM,CAAC;AACzD,UAAI,CAAC,qBAAqB,IAAI,MAAM,GAAG;AACrC,cAAM,IAAI;AAAA,UACR,iFACW,MAAM,eAAe,CAAC;AAAA,QAGnC;AAAA,MACF;AAAA,IACF;AAEA,WAAO,EAAE,UAAU;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,mBACZ,MACA,OACA,cACA,QACA,MACA,KAC0B;AAC1B,UAAM,aAAa,KAAK,kBAAkB,IAAI;AAC9C,UAAM,aAAa,IAAI,IAAI,KAAK,UAAU,IAAI,CAAC,OAAO,CAAC,GAAG,YAAY,EAAE,CAAC,CAAC;AAM1E,UAAM,YAAY,oBAAI,IAAoB;AAC1C,eAAW,MAAM,MAAM,kBAAkB,CAAC,GAAG;AAC3C,UAAI,GAAG,YAAa,WAAU,IAAI,GAAG,WAAW,GAAG,WAAW;AAAA,IAChE;AACA,UAAM,UAAyB,CAAC;AAChC,UAAM,gBAA0B,CAAC;AACjC,eAAW,OAAO,MAAM,cAAc,CAAC,GAAG;AACxC,YAAM,KAAK,WAAW,IAAI,GAAG;AAC7B,UAAI,IAAI;AACN,gBAAQ,KAAK,GAAG,OAAO;AACvB;AAAA,MACF;AACA,YAAM,QAAQ,KAAK,iBAAiB,MAAM,KAAK,WAAW;AAC1D,YAAM,OAAO,UAAU,IAAI,GAAG;AAC9B,cAAQ,KAAK,OAAO,EAAE,OAAO,iBAAiB,KAAK,IAAI,KAAK;AAC5D,oBAAc,KAAK,KAAK;AACxB,gBAAU,OAAO,GAAG;AAAA,IACtB;AACA,eAAW,CAAC,KAAK,IAAI,KAAK,WAAW;AACnC,YAAM,QAAQ,KAAK,iBAAiB,MAAM,KAAK,WAAW;AAC1D,cAAQ,KAAK,EAAE,OAAO,iBAAiB,KAAK,CAAC;AAC7C,oBAAc,KAAK,KAAK;AAAA,IAC1B;AAIA,UAAM,WAAW,MAAM,IAAI,iBAAkB,YAAY;AAAA,MACvD,SAAS,QAAQ,SAAS,IAAK,UAAkC;AAAA,MACjE,cAAc,aAAa,SAAS,IAAI,eAAe;AAAA,MACvD,QAAQ,KAAK,cAAc,YAAY,QAAQ,GAAG;AAAA,MAClD,UAAU,MAAM;AAAA,MAChB,SAAS,IAAI;AAAA,IACf,CAAC;AAKD,UAAM,eAAiC,CAAC;AACxC,eAAW,MAAM,KAAK,WAAW;AAC/B,YAAM,WAAW,CAAC,GAAG,IAAI,IAAI,SAAS,IAAI,CAAC,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,OAAO,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC;AACzF,YAAM,WAAW,MAAM,KAAK,cAAc,GAAG,WAAW,GAAG,MAAM,UAAU,GAAG;AAC9E,mBAAa,KAAK,EAAE,YAAY,GAAG,YAAY,SAAS,GAAG,SAAS,SAAS,CAAC;AAAA,IAChF;AAEA,UAAM,YAAgC,MAAM,YAAY,CAAC,GAAG,IAAI,CAAC,OAAO;AAAA,MACtE,OAAO;AAAA;AAAA,MAEP,QAAQ,KAAK,0BAA0B,MAAM,CAAC,EAAE;AAAA,IAClD,EAAE;AAEF,UAAM,SAAS,oBAAoB,UAAU,eAAe,cAAc,QAAQ;AAGlF,UAAM,aAAa,OAAO,IAAI,CAAC,QAAQ;AACrC,YAAM,MAA+B,CAAC;AAItC,iBAAW,OAAO,KAAK,oBAAoB,KAAK,GAAG;AACjD,YAAI,WAAW,IAAI,GAAG,GAAG;AACvB,cAAI,OAAO,IAAK,KAAI,GAAG,IAAI,IAAI,GAAG;AAAA,QACpC,OAAO;AACL,gBAAM,QAAQ,KAAK,iBAAiB,MAAM,KAAK,WAAW;AAC1D,cAAI,SAAS,IAAK,KAAI,GAAG,IAAI,IAAI,KAAK;AAAA,QACxC;AAAA,MACF;AACA,iBAAW,KAAK,MAAM,YAAY,CAAC,GAAG;AACpC,YAAI,KAAK,IAAK,KAAI,CAAC,IAAI,IAAI,CAAC;AAAA,MAC9B;AACA,aAAO;AAAA,IACT,CAAC;AAED,WAAO,EAAE,MAAM,YAAY,QAAQ,KAAK,eAAe,OAAO,IAAI,EAAE;AAAA,EACtE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,cACZ,WACA,MACA,UACA,KACgC;AAChC,UAAM,MAAM,oBAAI,IAAsB;AACtC,QAAI,SAAS,WAAW,KAAK,OAAO,IAAI,qBAAqB,WAAY,QAAO;AAChF,UAAM,WAAoC,EAAE,IAAI,EAAE,KAAK,SAAS,EAAE;AAClE,UAAM,QAAQ,OAAO,IAAI,iBAAiB,aAAa,IAAI,aAAa,SAAS,IAAI;AACrF,UAAM,SAAS,SAAS,OAAO,EAAE,MAAM,CAAC,UAAU,KAAK,EAAE,IAAI;AAC7D,UAAM,OAAO,MAAM,IAAI,iBAAiB,WAAW;AAAA,MACjD,SAAS,CAAC,MAAM,IAAI;AAAA,MACpB,cAAc,CAAC,EAAE,OAAO,MAAM,QAAQ,SAAS,OAAO,KAAK,CAAC;AAAA,MAC5D;AAAA,MACA,SAAS,IAAI;AAAA,IACf,CAAC;AACD,eAAW,KAAK,MAAM;AACpB,UAAI,EAAE,MAAM,KAAM,KAAI,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;AAAA,IACzC;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWQ,qBACN,KACA,UACA,QACA,QACe;AACf,QAAI,aAAa,MAAO,QAAO,GAAG,GAAG;AACrC,QAAI,aAAa,SAAU,QAAO,GAAG,GAAG;AAExC,QAAI,CAAC,UAAU,OAAO,WAAW,EAAG,QAAO;AAE3C,QAAI,aAAa,QAAQ,aAAa,SAAS;AAC7C,YAAM,eAAe,OAClB,IAAI,CAAC,MAAM;AAAE,eAAO,KAAK,6BAA6B,CAAC,CAAC;AAAG,eAAO,IAAI,OAAO,MAAM;AAAA,MAAI,CAAC,EACxF,KAAK,IAAI;AACZ,aAAO,GAAG,GAAG,IAAI,aAAa,OAAO,OAAO,QAAQ,KAAK,YAAY;AAAA,IACvE;AAEA,QAAI,aAAa,cAAc,aAAa,eAAe;AACzD,aAAO,KAAK,IAAI,OAAO,CAAC,CAAC,GAAG;AAC5B,aAAO,GAAG,GAAG,IAAI,aAAa,aAAa,SAAS,UAAU,KAAK,OAAO,MAAM;AAAA,IAClF;AAEA,UAAM,KAAK,eAAe,QAAQ;AAClC,QAAI,CAAC,GAAI,QAAO;AAChB,WAAO,KAAK,6BAA6B,OAAO,CAAC,CAAC,CAAC;AACnD,WAAO,GAAG,GAAG,IAAI,EAAE,KAAK,OAAO,MAAM;AAAA,EACvC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcQ,aACN,MACA,QACA,MAC4C;AAC5C,UAAM,MAAM,SAAS,cAAc,KAAK,aAAa,KAAK;AAC1D,QAAI,IAAI,MAAM,EAAG,QAAO,IAAI,MAAM;AAClC,QAAI,OAAO,SAAS,GAAG,GAAG;AACxB,YAAM,CAAC,OAAO,GAAG,IAAI,IAAI,OAAO,MAAM,GAAG;AACzC,YAAM,OAAO,KAAK,KAAK,GAAG;AAC1B,UAAI,UAAU,KAAK,QAAQ,IAAI,IAAI,EAAG,QAAO,IAAI,IAAI;AACrD,UAAI,IAAI,IAAI,EAAG,QAAO,IAAI,IAAI;AAC9B,YAAM,OAAO,OAAO,QAAQ,OAAO,GAAG;AACtC,UAAI,IAAI,IAAI,EAAG,QAAO,IAAI,IAAI;AAC9B,UAAI,SAAS,YAAa,QAAO,EAAE,KAAK,QAAQ,MAAM,SAAS;AAAA,IACjE,WAAW,IAAI,MAAM,GAAG;AACtB,aAAO,IAAI,MAAM;AAAA,IACnB;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,iBAAiB,MAAY,QAAgB,MAA+C;AAClG,QAAI,SAAS,eAAe,SAAS,OAAO;AAC1C,YAAM,MAAM,KAAK,aAAa,MAAM,QAAQ,WAAW;AACvD,UAAI,IAAK,QAAO,IAAI,IAAI,QAAQ,OAAO,EAAE;AAAA,IAC3C;AACA,QAAI,SAAS,aAAa,SAAS,OAAO;AACxC,YAAM,UAAU,KAAK,aAAa,MAAM,QAAQ,SAAS;AACzD,UAAI,QAAS,QAAO,QAAQ,IAAI,QAAQ,OAAO,EAAE;AAAA,IACnD;AACA,WAAO,OAAO,SAAS,GAAG,IAAI,OAAO,MAAM,GAAG,EAAE,CAAC,IAAI;AAAA,EACvD;AAAA,EAEQ,0BAA0B,MAAY,aAAwD;AACpG,UAAM,SAAS,KAAK,aAAa,MAAM,aAAa,SAAS;AAG7D,QAAI,QAAQ;AACV,aAAO;AAAA,QACL,OAAO,OAAO,IAAI,QAAQ,OAAO,EAAE;AAAA,QACnC,QAAQ,OAAO,SAAS,mBAAmB,mBAAmB,OAAO;AAAA,MACvE;AAAA,IACF;AAKA,UAAM,YAAY,YAAY,SAAS,GAAG,IAAI,YAAY,MAAM,GAAG,EAAE,CAAC,IAAI;AAC1E,UAAM,WAAW,CAAC,SAAS,OAAO,OAAO,OAAO,OAAO,gBAAgB;AACvE,eAAW,QAAQ,UAAU;AAC3B,YAAM,SAAS,IAAI,IAAI;AACvB,UAAI,UAAU,SAAS,MAAM,GAAG;AAC9B,cAAM,YAAY,UAAU,MAAM,GAAG,CAAC,OAAO,MAAM;AACnD,cAAM,YAAY,KAAK,SAAS,SAAS;AACzC,YAAI,aAAa,UAAU,SAAS,MAAM;AACxC,iBAAO;AAAA,YACL,OAAO,UAAU,IAAI,QAAQ,OAAO,EAAE;AAAA,YACtC,QAAQ,UAAU,SAAS,mBAAmB,mBAAmB,UAAU;AAAA,UAC7E;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACA,WAAO,EAAE,OAAO,KAAK,QAAQ,QAAQ;AAAA,EACvC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgCQ,gBACN,MACA,MACA,QACA,WACM;AACN,QAAI,CAAC,KAAM;AAEX,QAAI,KAAK,SAAS,QAAQ;AACxB,YAAM,YAAY,KAAK,iBAAiB,MAAM,KAAK,QAAQ,KAAK;AAChE,YAAM,QAAQ,KAAK,mBAAmB,QAAQ,WAAW,KAAK,cAAc,KAAK,UAAU,KAAK,MAAM,CAAC;AACvG,UAAI,MAAO,WAAU,KAAK,KAAK;AAC/B;AAAA,IACF;AAEA,QAAI,KAAK,SAAS,OAAO;AACvB,iBAAW,SAAS,KAAK,SAAU,MAAK,gBAAgB,OAAO,MAAM,QAAQ,SAAS;AACtF;AAAA,IACF;AAEA,UAAM,WAAW,KAAK,sBAAsB,MAAM,IAAI;AACtD,QAAI,SAAU,WAAU,KAAK,QAAQ;AAAA,EACvC;AAAA;AAAA,EAGQ,sBACN,MACA,MACgC;AAChC,QAAI,CAAC,KAAM,QAAO;AAElB,QAAI,KAAK,SAAS,OAAO;AACvB,YAAM,QAAQ,KAAK,sBAAsB,KAAK,OAAO,IAAI;AACzD,aAAO,QAAQ,EAAE,MAAM,MAAM,IAAI;AAAA,IACnC;AAEA,QAAI,KAAK,SAAS,MAAM;AACtB,YAAM,WAAW,KAAK,SACnB,IAAI,CAAC,UAAU,KAAK,sBAAsB,OAAO,IAAI,CAAC,EACtD,OAAO,CAAC,MAAoC,CAAC,CAAC,CAAC;AAClD,aAAO,SAAS,SAAS,IAAI,EAAE,KAAK,SAAS,IAAI;AAAA,IACnD;AAIA,UAAM,SAAkC,CAAC;AACzC,UAAM,YAAuC,CAAC;AAC9C,SAAK,gBAAgB,MAAM,MAAM,QAAQ,SAAS;AAClD,QAAI,UAAU,SAAS,GAAG;AACxB,aAAO,OAAO,CAAC,GAAI,MAAM,QAAQ,OAAO,IAAI,IAAI,OAAO,OAAO,CAAC,GAAI,GAAG,SAAS;AAAA,IACjF;AACA,WAAO,OAAO,KAAK,MAAM,EAAE,SAAS,IAAI,SAAS;AAAA,EACnD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,oBACN,MACA,MACA,QACe;AACf,QAAI,CAAC,KAAM,QAAO;AAElB,QAAI,KAAK,SAAS,QAAQ;AACxB,aAAO,KAAK;AAAA,QACV,KAAK,iBAAiB,MAAM,KAAK,QAAQ,KAAK;AAAA,QAC9C,KAAK;AAAA,QACL,KAAK;AAAA,QACL;AAAA,MACF;AAAA,IACF;AAEA,QAAI,KAAK,SAAS,OAAO;AACvB,YAAM,QAAQ,KAAK,oBAAoB,KAAK,OAAO,MAAM,MAAM;AAC/D,aAAO,QAAQ,QAAQ,KAAK,MAAM;AAAA,IACpC;AAEA,UAAM,QAAQ,KAAK,SAChB,IAAI,CAAC,UAAU,KAAK,oBAAoB,OAAO,MAAM,MAAM,CAAC,EAC5D,OAAO,CAAC,MAAmB,CAAC,CAAC,CAAC;AACjC,QAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAI,MAAM,WAAW,EAAG,QAAO,MAAM,CAAC;AACtC,WAAO,IAAI,MAAM,KAAK,KAAK,SAAS,OAAO,SAAS,OAAO,CAAC;AAAA,EAC9D;AAAA,EAEQ,mBACN,QACA,OACA,SACgC;AAChC,UAAM,WAAW,OAAO,KAAK;AAC7B,QAAI,aAAa,QAAW;AAC1B,aAAO,KAAK,IAAI;AAChB,aAAO;AAAA,IACT;AACA,UAAM,YAAY,CAAC,MACjB,CAAC,CAAC,KAAK,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,CAAC;AAClD,QAAI,CAAC,UAAU,QAAQ,KAAK,CAAC,UAAU,OAAO,EAAG,QAAO,EAAE,CAAC,KAAK,GAAG,QAAQ;AAC3E,QAAI,OAAO,KAAK,OAAO,EAAE,KAAK,CAAC,OAAO,MAAM,QAAQ,EAAG,QAAO,EAAE,CAAC,KAAK,GAAG,QAAQ;AACjF,WAAO,KAAK,IAAI,EAAE,GAAG,UAAU,GAAG,QAAQ;AAC1C,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyCQ,gBACN,MACA,OAC2D;AAC3D,UAAM,MAAiE,CAAC;AACxE,eAAW,MAAM,MAAM,kBAAkB,CAAC,GAAG;AAC3C,UAAI,CAAC,GAAG,UAAW;AACnB,YAAM,QAAQ,MAAM,QAAQ,GAAG,SAAS,IAAI,GAAG,YAAY,CAAC,GAAG,WAAW,GAAG,SAAS;AACtF,YAAM,CAAC,OAAO,MAAM,KAAK,IAAI;AAC7B,UAAI,SAAS,KAAM;AACnB,UAAI,KAAK;AAAA,QACP,OAAO,KAAK,iBAAiB,MAAM,GAAG,WAAW,WAAW;AAAA,QAC5D,QAAQ;AAAA,UACN,MAAM,6BAA6B,OAAO,KAAK,CAAC;AAAA,UAChD,MAAM,6BAA6B,OAAO,GAAG,CAAC;AAAA,QAChD;AAAA,MACF,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,cAAc,UAAkB,QAA4B;AAClE,QAAI,aAAa,MAAO,QAAO,EAAE,KAAK,KAAK;AAC3C,QAAI,aAAa,SAAU,QAAO;AAClC,QAAI,CAAC,UAAU,OAAO,WAAW,EAAG,QAAO;AAE3C,UAAM,KAAK,6BAA6B,OAAO,CAAC,CAAC;AACjD,UAAM,MAAM,OAAO,IAAI,4BAA4B;AACnD,YAAQ,UAAU;AAAA,MAChB,KAAK;AAAU,eAAO;AAAA,MACtB,KAAK;AAAa,eAAO,EAAE,KAAK,GAAG;AAAA,MACnC,KAAK;AAAM,eAAO,EAAE,KAAK,GAAG;AAAA,MAC5B,KAAK;AAAO,eAAO,EAAE,MAAM,GAAG;AAAA,MAC9B,KAAK;AAAM,eAAO,EAAE,KAAK,GAAG;AAAA,MAC5B,KAAK;AAAO,eAAO,EAAE,MAAM,GAAG;AAAA,MAC9B,KAAK;AAAY,eAAO,EAAE,QAAQ,OAAO,CAAC,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAM5C,KAAK;AAAe,eAAO,EAAE,cAAc,OAAO,CAAC,EAAE;AAAA,MACrD,KAAK;AAAc,eAAO,EAAE,aAAa,OAAO,CAAC,EAAE;AAAA,MACnD,KAAK;AAAY,eAAO,EAAE,WAAW,OAAO,CAAC,EAAE;AAAA,MAC/C,KAAK;AAAM,eAAO,EAAE,KAAK,IAAI;AAAA,MAC7B,KAAK;AAAS,eAAO,EAAE,MAAM,IAAI;AAAA,MACjC;AAKE,cAAM,IAAI;AAAA,UACR,iEAAiE,QAAQ;AAAA,QAE3E;AAAA,IACJ;AAAA,EACF;AAAA,EAEQ,kBAAkB,MAAoB;AAC5C,WAAO,KAAK,IAAI,KAAK;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoBQ,oBAAoB,OAAiC;AAC3D,UAAM,MAAM,CAAC,GAAI,MAAM,cAAc,CAAC,CAAE;AACxC,eAAW,MAAM,MAAM,kBAAkB,CAAC,GAAG;AAC3C,UAAI,GAAG,eAAe,CAAC,IAAI,SAAS,GAAG,SAAS,EAAG,KAAI,KAAK,GAAG,SAAS;AAAA,IAC1E;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,eAAe,OAAuB,MAAmD;AAC/F,UAAM,SAAgD,CAAC;AACvD,eAAW,OAAO,KAAK,oBAAoB,KAAK,GAAG;AACjD,YAAM,IAAI,KAAK,aAAa,MAAM,KAAK,WAAW;AAClD,aAAO,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,QAAQ,SAAS,CAAC;AAAA,IACtD;AACA,QAAI,MAAM,UAAU;AAClB,iBAAW,KAAK,MAAM,UAAU;AAC9B,eAAO,KAAK,EAAE,MAAM,GAAG,MAAM,SAAS,CAAC;AAAA,MACzC;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACF;;;AE79BA,SAAS,2BAA2B;AAqB7B,IAAM,yBAAyB,oBAAI,IAAI,CAAC,aAAa,YAAY,CAAC;AAYlE,IAAM,uBAAiC,oBAAoB,QAC/D,OAAO,CAAC,MAAc,CAAC,uBAAuB,IAAI,CAAC,CAAC;AAoDvD,SAAS,sBAAsB,GAAmC;AAGhE,MAAI,CAAC,EAAE,WAAW;AAChB,UAAM,IAAI,MAAM,2CAA2C,EAAE,IAAI,oBAAoB;AAAA,EACvF;AACA,MAAI,uBAAuB,IAAI,EAAE,SAAS,GAAG;AAC3C,UAAM,IAAI;AAAA,MACR,+BAA+B,EAAE,IAAI,qBAAqB,EAAE,SAAS,kEACd,qBAAqB,KAAK,IAAI,CAAC;AAAA,IACxF;AAAA,EACF;AACA,SAAO,EAAE;AACX;AAGA,SAAS,cAAc,GAA4C;AACjE,UAAQ,EAAE,MAAM;AAAA,IACd,KAAK;AAAQ,aAAO;AAAA,IACpB,KAAK;AAAU,aAAO;AAAA,IACtB,KAAK;AAAW,aAAO;AAAA,IACvB,KAAK;AAAU,aAAO;AAAA,IACtB,KAAK;AAAU,aAAO;AAAA,IACtB;AAAS,aAAO;AAAA,EAClB;AACF;AAKA,SAAS,sBAAsB,OAA8B;AAC3D,QAAM,MAAM,MAAM,YAAY,GAAG;AACjC,SAAO,MAAM,IAAI,MAAM,MAAM,GAAG,GAAG,IAAI;AACzC;AAKA,IAAM,gBAAgB;AAOtB,IAAM,YAAY,CAAC,SAAyB,KAAK,QAAQ,OAAO,IAAI;AAE7D,SAAS,eACd,SACA,UACiB;AACjB,QAAM,UAAU,QAAQ,WAAW,CAAC;AAUpC,QAAM,aAAa,CAAC,YAAoB,QAAoC;AAC1E,QAAI,CAAC,SAAU,QAAO,EAAE,QAAQ,KAAK,OAAO,IAAI;AAChD,UAAM,WAAW,SAAS,YAAY,GAAG;AACzC,QAAI,CAAC,UAAU;AACb,YAAM,IAAI;AAAA,QACR,+BAA+B,QAAQ,IAAI,4BAA4B,GAAG,qCACvC,UAAU;AAAA,MAC/C;AAAA,IACF;AACA,WAAO,OAAO,aAAa,WAAW,EAAE,QAAQ,UAAU,OAAO,SAAS,IAAI;AAAA,EAChF;AACA,QAAM,QAAkC,CAAC;AACzC,aAAW,QAAQ,SAAS;AAC1B,UAAM,WAAW,KAAK,MAAM,GAAG;AAC/B,QAAI,SAAS,SAAS,eAAe;AACnC,YAAM,IAAI;AAAA,QACR,+BAA+B,QAAQ,IAAI,mBAAmB,IAAI,iBAC/D,aAAa,eAAe,SAAS,MAAM;AAAA,MAChD;AAAA,IACF;AACA,QAAI,aAAa,QAAQ;AACzB,QAAI,cAAc,QAAQ;AAC1B,QAAI,SAAS;AACb,eAAW,OAAO,UAAU;AAC1B,eAAS,SAAS,GAAG,MAAM,IAAI,GAAG,KAAK;AACvC,YAAM,SAAS,WAAW,YAAY,GAAG;AACzC,YAAM,QAAQ,UAAU,MAAM;AAC9B,UAAI,CAAC,MAAM,KAAK,GAAG;AAGjB,cAAM,KAAK,IAAI;AAAA,UACb,MAAM,OAAO;AAAA,UACb,cAAc;AAAA,UACd,KAAK,GAAG,WAAW,IAAI,GAAG,MAAM,MAAM;AAAA,QACxC;AAAA,MACF;AACA,mBAAa,OAAO;AACpB,oBAAc;AAAA,IAChB;AAAA,EACF;AAIA,QAAM,uBAAuB,IAAI,IAAI,OAAO,KAAK,KAAK,CAAC;AAGvD,QAAM,iBAAiB,CAAC,OAAe,WAAmB,cAAsB;AAC9E,UAAM,UAAU,sBAAsB,KAAK;AAC3C,QAAI,WAAW,CAAC,MAAM,UAAU,OAAO,CAAC,GAAG;AACzC,YAAM,IAAI;AAAA,QACR,sBAAsB,SAAS,KAAK,SAAS,mCAAmC,OAAO,UAC/E,KAAK,WAAW,OAAO;AAAA,MAEjC;AAAA,IACF;AAAA,EACF;AAGA,QAAM,aAA4C,CAAC;AACnD,aAAW,KAAK,QAAQ,YAAY;AAClC,mBAAe,EAAE,OAAO,aAAa,EAAE,IAAI;AAC3C,UAAM,MAAqB;AAAA,MACzB,MAAM,EAAE;AAAA,MACR,OAAO,OAAO,EAAE,UAAU,WAAW,EAAE,QAAQ,EAAE;AAAA,MACjD,MAAM,cAAc,CAAC;AAAA,MACrB,KAAK,EAAE;AAAA,IACT;AACA,QAAI,IAAI,SAAS,QAAQ;AACvB,UAAI,gBAAgB,EAAE,kBAClB,CAAC,EAAE,eAAe,IAClB,CAAC,OAAO,QAAQ,SAAS,WAAW,MAAM;AAAA,IAChD;AACA,eAAW,EAAE,IAAI,IAAI;AAAA,EACvB;AAGA,QAAM,WAAmC,CAAC;AAC1C,QAAM,UAAgC,CAAC;AACvC,QAAM,iBAAkD,CAAC;AAEzD,aAAW,KAAK,QAAQ,UAAU;AAChC,QAAI,EAAE,SAAS;AACb,cAAQ,KAAK,EAAE,MAAM,EAAE,MAAM,IAAI,EAAE,QAAQ,IAAI,IAAI,EAAE,QAAQ,GAAG,CAAC;AACjE;AAAA,IACF;AACA,QAAI,EAAE,MAAO,gBAAe,EAAE,OAAO,WAAW,EAAE,IAAI;AACtD,UAAM,SAAiB;AAAA,MACrB,MAAM,EAAE;AAAA,MACR,OAAO,OAAO,EAAE,UAAU,WAAW,EAAE,QAAQ,EAAE;AAAA,MACjD,MAAM,sBAAsB,CAAC;AAAA;AAAA,MAE7B,KAAK,EAAE,SAAS;AAAA,IAClB;AACA,QAAI,OAAO,EAAE,WAAW,SAAU,QAAO,SAAS,EAAE;AACpD,aAAS,EAAE,IAAI,IAAI;AACnB,QAAI,EAAE,OAAQ,gBAAe,EAAE,IAAI,IAAI,EAAE;AAAA,EAC3C;AAEA,QAAM,OAAa;AAAA,IACjB,MAAM,QAAQ;AAAA,IACd,OAAO,OAAO,QAAQ,UAAU,WAAW,QAAQ,QAAQ,QAAQ;AAAA,IACnE,KAAK,QAAQ;AAAA,IACb;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,EACV;AACA,MAAI,OAAO,KAAK,KAAK,EAAE,SAAS,EAAG,MAAK,QAAQ;AAEhD,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ,QAAQ;AAAA,IAChB;AAAA,EACF;AACF;;;AChQA,SAAS,0BAAgD;AAEzD,SAAS,wBAAwB,2BAA2B;AAqF5D,SAAS,uBACP,UACA,WACA,SAC4D;AAK5D,QAAM,WAAW,uBAAuB,SAAS,oBAAI,KAAK,CAAC;AAC3D,QAAM,UAAU,CAAI,MAAY,oBAAoB,GAAG,QAAQ;AAE/D,QAAM,SAAS,QAAQ,SAAS,MAAM;AACtC,QAAM,iBAAiB,QAAQ,SAAS,cAAc;AACtD,QAAM,gBAAgB,QAAQ,UAAU,aAAa;AACrD,QAAM,iBAAiB,UAAU,gBAAgB;AAAA,IAAI,CAAC,OACpD,GAAG,aAAa,OAAO,KAAK,EAAE,GAAG,IAAI,WAAW,QAAQ,GAAG,SAAS,EAAE;AAAA,EACxE;AAEA,QAAM,kBACJ,WAAW,SAAS,UAAU,mBAAmB,SAAS;AAC5D,QAAM,mBACJ,kBAAkB,UAAU,iBAC3B,mBAAmB,UAClB,eAAe,KAAK,CAAC,IAAI,MAAM,OAAO,UAAU,eAAgB,CAAC,CAAC;AAEtE,SAAO;AAAA,IACL,UAAU,kBAAkB,EAAE,GAAG,UAAU,QAAQ,eAAe,IAAI;AAAA,IACtE,WAAW,mBAAmB,EAAE,GAAG,WAAW,eAAe,eAAe,IAAI;AAAA,EAClF;AACF;AAGO,SAAS,eACd,GACA,GAC6B;AAC7B,MAAI,KAAK,EAAG,QAAO,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE;AAClC,SAAO,KAAK;AACd;AAsBO,SAAS,sBACd,UACA,gBAC8C;AAC9C,QAAM,aAAuB,CAAC;AAC9B,QAAM,WAAqB,CAAC;AAC5B,aAAW,KAAK,SAAU,EAAC,eAAe,CAAC,IAAI,WAAW,YAAY,KAAK,CAAC;AAC5E,SAAO,EAAE,YAAY,SAAS;AAChC;AAMO,SAAS,wBACd,MACA,SAC2B;AAC3B,MAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,SAAO,KAAK,IAAI,CAAC,QAAQ;AACvB,UAAM,MAAM,EAAE,GAAG,IAAI;AACrB,eAAW,KAAK,SAAS;AACvB,UAAI,EAAE,IAAI,IAAI,eAAe,GAAG,GAAG;AAAA,IACrC;AACA,WAAO;AAAA,EACT,CAAC;AACH;AA6CO,SAAS,gBACd,MACA,kBAC2B;AAC3B,aAAW,CAAC,QAAQC,UAAS,KAAK,OAAO,QAAQ,gBAAgB,GAAG;AAClE,UAAM,QAAQ,mBAAmBA,UAAS;AAC1C,QAAI,UAAU,OAAW;AACzB,eAAW,OAAO,KAAM,KAAI,IAAI,MAAM,KAAK,KAAM,KAAI,MAAM,IAAI;AAAA,EACjE;AACA,SAAO;AACT;AAEA,SAAS,IAAI,GAA2B;AACtC,MAAI,KAAK,KAAM,QAAO;AACtB,QAAM,IAAI,OAAO,MAAM,WAAW,IAAI,OAAO,CAAC;AAC9C,SAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAClC;AAEA,SAAS,eAAe,GAAuB,KAA6C;AAC1F,QAAM,OAAO,EAAE,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,IAAI,CAAC,CAAC;AAC9C,MAAI,KAAK,KAAK,CAAC,MAAM,MAAM,IAAI,EAAG,QAAO;AACzC,QAAM,OAAO;AACb,UAAQ,EAAE,IAAI;AAAA,IACZ,KAAK,SAAS;AACZ,UAAI,KAAK,SAAS,KAAK,KAAK,CAAC,MAAM,EAAG,QAAO;AAC7C,aAAO,KAAK,CAAC,IAAI,KAAK,CAAC;AAAA,IACzB;AAAA,IACA,KAAK;AACH,aAAO,KAAK,MAAM,CAAC,EAAE,OAAO,CAAC,KAAK,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC;AAAA,IAC1D,KAAK;AACH,aAAO,KAAK,OAAO,CAAC,KAAK,MAAM,MAAM,GAAG,CAAC;AAAA,IAC3C,KAAK;AACH,aAAO,KAAK,OAAO,CAAC,KAAK,MAAM,MAAM,GAAG,CAAC;AAAA,IAC3C;AACE,aAAO;AAAA,EACX;AACF;AAkCO,SAAS,4BACd,WACA,WACA,gBACkC;AAKlC,QAAM,UAAU,UAAU,kBAAkB,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,cAAc,SAAS,GAAG;AACxF,MAAI,OAAQ,QAAO;AACnB,SAAO,UAAU,mBAAoB;AACvC;AAiBA,SAAS,cAAc,GAAY,GAAoB;AACrD,QAAM,QAAQ,KAAK,QAAQ,MAAM;AACjC,QAAM,QAAQ,KAAK,QAAQ,MAAM;AACjC,MAAI,SAAS,MAAO,QAAO,SAAS,QAAQ,IAAI,QAAQ,IAAI;AAC5D,MAAI,aAAa,QAAQ,aAAa,MAAM;AAC1C,WAAO,OAAO,aAAa,OAAO,EAAE,QAAQ,IAAI,CAAC,IAAI,OAAO,aAAa,OAAO,EAAE,QAAQ,IAAI,CAAC;AAAA,EACjG;AACA,MAAI,OAAO,MAAM,aAAa,OAAO,MAAM,WAAW;AACpD,WAAO,OAAO,CAAC,IAAI,OAAO,CAAC;AAAA,EAC7B;AACA,QAAM,KAAK,OAAO,MAAM,WAAW,IAAI,OAAO,CAAC;AAC/C,QAAM,KAAK,OAAO,MAAM,WAAW,IAAI,OAAO,CAAC;AAC/C,MAAI,OAAO,SAAS,EAAE,KAAK,OAAO,SAAS,EAAE,EAAG,QAAO,KAAK;AAC5D,SAAO,OAAO,CAAC,EAAE,cAAc,OAAO,CAAC,CAAC;AAC1C;AAaO,SAAS,cACd,MACA,OACA,UAC2B;AAC3B,QAAM,OAAO,OAAO,QAAQ,SAAS,CAAC,CAAC;AACvC,MAAI,KAAK,WAAW,KAAK,KAAK,SAAS,EAAG,QAAO;AAGjD,SAAO,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,IAAI,OAAO;AAChC,eAAW,CAAC,KAAK,GAAG,KAAK,MAAM;AAC7B,YAAM,MAAM,WAAW,GAAG;AAC1B,YAAM,KAAK,KAAK,IAAI,GAAG,GAAG,CAAC,KAAK,GAAG,GAAG;AACtC,YAAM,KAAK,KAAK,IAAI,GAAG,GAAG,CAAC,KAAK,GAAG,GAAG;AACtC,YAAM,QAAQ,MAAM,QAAQ,OAAO;AACnC,YAAM,QAAQ,MAAM,QAAQ,OAAO;AAEnC,UAAI,SAAS,OAAO;AAClB,YAAI,SAAS,MAAO;AACpB,eAAO,QAAQ,IAAI;AAAA,MACrB;AACA,YAAM,IAAI,cAAc,IAAI,EAAE;AAC9B,UAAI,MAAM,EAAG,QAAO,QAAQ,SAAS,CAAC,IAAI;AAAA,IAC5C;AACA,WAAO;AAAA,EACT,CAAC;AACH;AAGO,SAAS,YACd,MACA,OACA,QAC2B;AAC3B,QAAM,QAAQ,UAAU,QAAQ,SAAS,IAAI,SAAS;AACtD,MAAI,UAAU,KAAK,SAAS,KAAM,QAAO;AACzC,SAAO,KAAK,MAAM,OAAO,SAAS,OAAO,QAAQ,QAAQ,MAAS;AACpE;AA8BO,SAAS,gBACd,WACA,YACA,iBAA2B,CAAC,GACgB;AAC5C,QAAM,QAAQ,UAAU;AACxB,MAAI,SAAS,OAAO,KAAK,KAAK,EAAE,SAAS,GAAG;AAC1C,UAAM,aAAa,oBAAI,IAAY;AAAA,MACjC,GAAG;AAAA,MACH,GAAG,UAAU;AAAA,MACb,GAAG,UAAU,SAAS,IAAI,CAAC,MAAM,GAAG,CAAC,WAAW;AAAA,IAClD,CAAC;AACD,UAAM,UAAU,OAAO,KAAK,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,IAAI,CAAC,CAAC;AACnE,QAAI,QAAQ,QAAQ;AAClB,YAAM,IAAI;AAAA,QACR,mCAAmC,QAAQ,IAAI,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,KAAK,IAAI,CAAC,iEAEvE,CAAC,GAAG,UAAU,EAAE,KAAK,IAAI,KAAK,QAAQ;AAAA,MAC3C;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,OAAK,UAAU,SAAS,QAAQ,UAAU,UAAU,SAAS,WAAW,SAAS,GAAG;AAClF,WAAO,OAAO,YAAY,WAAW,IAAI,CAAC,MAAM,CAAC,GAAG,KAAc,CAAC,CAAC;AAAA,EACtE;AAEA,QAAM,WAAW,eAAe,OAAO,CAAC,MAAM,WAAW,SAAS,CAAC,CAAC;AACpE,MAAI,SAAS,SAAS,GAAG;AACvB,WAAO,OAAO,YAAY,SAAS,IAAI,CAAC,MAAM,CAAC,GAAG,KAAc,CAAC,CAAC;AAAA,EACpE;AACA,SAAO;AACT;AAIA,SAAS,SAAS,MAAsB;AAEtC,QAAM,KAAK,KAAK,MAAM,KAAK,WAAW,KAAK,GAAG,IAAI,eAAe,IAAI;AACrE,MAAI,OAAO,MAAM,EAAE,EAAG,OAAM,IAAI,MAAM,kDAAkD,IAAI,GAAG;AAC/F,SAAO;AACT;AAEA,IAAM,SAAS;AAEf,SAAS,UAAU,IAAoB;AACrC,SAAO,IAAI,KAAK,EAAE,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAC/C;AAEA,SAAS,UAAU,MAAc,OAAuB;AACtD,QAAM,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC;AACjC,IAAE,eAAe,EAAE,eAAe,IAAI,KAAK;AAC3C,SAAO,UAAU,EAAE,QAAQ,CAAC;AAC9B;AAGO,SAAS,WAAW,OAAyB,MAA2C;AAC7F,QAAM,CAAC,OAAO,GAAG,IAAI;AACrB,MAAI,SAAS,gBAAgB;AAC3B,WAAO,CAAC,UAAU,OAAO,EAAE,GAAG,UAAU,KAAK,EAAE,CAAC;AAAA,EAClD;AAEA,QAAM,UAAU,SAAS,KAAK;AAC9B,QAAM,QAAQ,SAAS,GAAG;AAC1B,QAAM,aAAa,KAAK,OAAO,QAAQ,WAAW,MAAM,IAAI;AAC5D,QAAM,YAAY,UAAU;AAC5B,QAAM,cAAc,aAAa,aAAa,KAAK;AACnD,SAAO,CAAC,UAAU,WAAW,GAAG,UAAU,SAAS,CAAC;AACtD;AAEO,IAAM,kBAAN,MAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAS3B,YACmB,SACA,aACjB;AAFiB;AACA;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASH,MAAM,QACJ,eACA,gBACA,SAC0B;AAI1B,UAAM,EAAE,UAAU,UAAU,IAAI,uBAAuB,eAAe,gBAAgB,OAAO;AAE7F,UAAM,SAAS,MAAM,KAAK,iBAAiB,UAAU,WAAW,OAAO;AASvE,UAAM,YAAY,UAAU,QAAQ;AACpC,QAAI,WAAW,QAAQ;AACrB,YAAM,WAAW,IAAI,IAAI,UAAU,cAAc,CAAC,CAAC;AACnD,YAAM,SAAiD,CAAC;AACxD,iBAAW,YAAY,WAAW;AAChC,cAAM,UAAU,SAAS,OAAO,CAAC,MAAM,CAAC,SAAS,IAAI,CAAC,CAAC;AACvD,YAAI,QAAQ,QAAQ;AAClB,gBAAM,IAAI;AAAA,YACR,uCAAuC,SAAS,KAAK,IAAI,CAAC,gEAA2D,QAAQ,KAAK,IAAI,CAAC;AAAA,UACzI;AAAA,QACF;AACA,cAAM,MAAM,MAAM,KAAK,iBAAiB,UAAU;AAAA,UAChD,GAAG;AAAA,UACH,YAAY;AAAA,UACZ,QAAQ;AAAA,UACR,OAAO;AAAA,UACP,OAAO;AAAA,UACP,QAAQ;AAAA,QACV,GAAG,OAAO;AACV,eAAO,KAAK,EAAE,YAAY,UAAU,MAAM,IAAI,KAAK,CAAC;AAAA,MACtD;AACA,aAAO,SAAS;AAAA,IAClB;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,iBACZ,UACA,WACA,SAC0B;AAC1B,UAAM,gBAAgB,IAAI,IAAI,SAAS,QAAQ,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AACtE,UAAM,kBAAkB,UAAU,SAC/B,IAAI,CAAC,MAAM,cAAc,IAAI,CAAC,CAAC,EAC/B,OAAO,CAAC,MAA+B,CAAC,CAAC,CAAC;AAG7C,UAAM,eAAe,oBAAI,IAAY;AACrC,eAAW,KAAK,UAAU,UAAU;AAClC,UAAI,CAAC,cAAc,IAAI,CAAC,EAAG,cAAa,IAAI,CAAC;AAAA,IAC/C;AACA,eAAW,KAAK,iBAAiB;AAC/B,iBAAW,OAAO,EAAE,GAAI,cAAa,IAAI,GAAG;AAAA,IAC9C;AAGA,UAAM,EAAE,YAAY,SAAS,IAAI,sBAAsB,cAAc,SAAS,cAAc;AAE5F,UAAM,aAAa,eAAe,SAAS,QAAQ,UAAU,aAAa;AAC1E,UAAM,aAAa,UAAU,cAAc,CAAC;AAK5C,UAAM,QAAQ,gBAAgB,WAAW,YAAY,KAAK,iBAAiB,UAAU,UAAU,CAAC;AAOhG,UAAM,iBAAiB,KAAK,cACxB,OAAO,KAAK,SAAS,CAAC,CAAC,EAAE;AAAA,MACvB,CAAC,MAAM,WAAW,SAAS,CAAC,KAAK,KAAK,YAAa,eAAe,CAAC;AAAA,IACrE,IACA,CAAC;AAQL,UAAM,cAAc,SAAS,WAAW,KAAK,CAAC,UAAU,aAAa,gBAAgB,WAAW;AAChG,UAAM,eAAe,oBAAI,IAAY,CAAC,GAAG,YAAY,GAAG,UAAU,CAAC;AACnE,UAAM,oBACJ,eAAe,eAAe,WAAW,KACzC,OAAO,KAAK,SAAS,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,aAAa,IAAI,CAAC,CAAC;AAC3D,UAAM,cAAc,oBAChB,EAAE,OAAO,OAAO,UAAU,OAAO,QAAQ,UAAU,OAAO,IAC1D;AAIJ,UAAM,SAAS,MAAM,KAAK,eAAe,UAAU,WAAW;AAAA,MAC5D,UAAU,CAAC,GAAG,YAAY;AAAA,MAC1B;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,MACR;AAAA,IACF,CAAC;AAGD,QAAI,UAAU,WAAW;AACvB,YAAM,cAAc,MAAM,KAAK,WAAW,UAAU,WAAW,CAAC,GAAG,YAAY,GAAG,YAAY,YAAY,OAAO;AACjH,aAAO,OAAO;AAAA,QACZ,OAAO;AAAA,QACP;AAAA,QACA;AAAA,QACA,CAAC,GAAG,YAAY,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,WAAW;AAAA,MAC9C;AACA,iBAAW,KAAK,aAAc,QAAO,OAAO,KAAK,EAAE,MAAM,GAAG,CAAC,aAAa,MAAM,SAAS,CAAC;AAAA,IAC5F;AAsBA,UAAM,cAAkD,CAAC;AACzD,eAAW,KAAK,cAAc;AAC5B,YAAMA,aAAY,SAAS,KAAK,WAAW,CAAC,GAAG;AAC/C,kBAAY,CAAC,IAAIA;AACjB,UAAI,UAAU,UAAW,aAAY,GAAG,CAAC,WAAW,IAAIA;AAAA,IAC1D;AACA,oBAAgB,OAAO,MAAM,WAAW;AAGxC,WAAO,OAAO,wBAAwB,OAAO,MAAM,eAAe;AAClE,eAAW,KAAK,gBAAiB,QAAO,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,SAAS,CAAC;AAgBpF,QAAI;AACJ,eAAW,OAAO,gBAAgB;AAChC,YAAM,SAAS,CAAC,GAAG,IAAI,IAAI,OAAO,KAAK,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC;AACnF,UAAI,OAAO,WAAW,EAAG;AACzB,YAAM,SAAS,MAAM,KAAK,YAAa,cAAc,KAAK,MAAM;AAChE,UAAI,UAAU,OAAO,OAAO,EAAG,EAAC,wBAAa,CAAC,IAAG,GAAG,IAAI;AAAA,IAC1D;AACA,WAAO,OAAO,cAAc,OAAO,MAAM,OAAO,QAAQ;AACxD,WAAO,OAAO,YAAY,OAAO,MAAM,UAAU,OAAO,UAAU,MAAM;AAExE,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgCA,MAAc,eACZ,UACA,WACA,MAO0B;AAC1B,UAAM,EAAE,UAAU,YAAY,YAAY,QAAQ,QAAQ,IAAI;AAC9D,UAAM,EAAE,YAAY,SAAS,IAAI,sBAAsB,UAAU,SAAS,cAAc;AAIxF,QAAI;AACJ,QAAI,WAAW,SAAS,KAAK,SAAS,WAAW,GAAG;AAClD,eAAS,MAAM,KAAK,QAAQ,MAAM,KAAK,WAAW,UAAU;AAAA,QAC1D,UAAU;AAAA,QACV;AAAA,QACA,OAAO;AAAA,QACP;AAAA,QACA,iBAAiB,SAAS;AAAA,QAC1B;AAAA,MACF,CAAC,GAAG,OAAO;AAAA,IACb,OAAO;AACL,eAAS,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,EAAE;AAAA,IAClC;AAGA,eAAW,KAAK,UAAU;AACxB,YAAM,UAAU,eAAe,YAAY,SAAS,eAAe,CAAC,CAAC;AACrE,YAAM,MAAM,MAAM,KAAK,QAAQ,MAAM,KAAK,WAAW,UAAU;AAAA,QAC7D,UAAU,CAAC,CAAC;AAAA,QAAG;AAAA,QAAY,OAAO;AAAA,QAAS;AAAA,QAC3C,iBAAiB,SAAS;AAAA,MAC5B,CAAC,GAAG,OAAO;AACX,aAAO,OAAO,kBAAkB,OAAO,MAAM,IAAI,MAAM,YAAY,CAAC,CAAC,CAAC;AACtE,aAAO,OAAO,KAAK,EAAE,MAAM,GAAG,MAAM,SAAS,CAAC;AAAA,IAChD;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYQ,iBAAiB,UAA2B,YAAgC;AAClF,WAAO,WAAW,OAAO,CAAC,MAAM,SAAS,KAAK,WAAW,CAAC,GAAG,SAAS,MAAM;AAAA,EAC9E;AAAA,EAEQ,WACN,UACA,MAcgB;AAChB,UAAM,IAAoB;AAAA,MACxB,MAAM,SAAS,KAAK;AAAA,MACpB,UAAU,KAAK;AAAA,MACf,YAAY,KAAK;AAAA;AAAA;AAAA,MAGjB,UAAU,KAAK,UAAU,YAAY,KAAK,mBAAmB;AAAA,IAC/D;AACA,QAAI,KAAK,MAAO,GAAE,QAAQ,KAAK;AAsB/B,UAAM,cAAc,KAAK,UAAU,kBAAkB,CAAC;AACtD,UAAM,UAAU,IAAI,IAAI,YAAY,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC;AAC3D,UAAM,iBAAiB,CAAC,SAAqC;AAC3D,YAAM,KAAK,SAAS,KAAK,WAAW,IAAI;AACxC,UAAI,IAAI,SAAS,OAAQ,QAAO;AAChC,YAAM,iBAAiB,GAAG,eAAe,WAAW,IAAI,OAAO,GAAG,cAAc,CAAC,CAAC,IAAI;AACtF,aAAO,4BAA4B,KAAK,WAAW,MAAM,cAAc;AAAA,IACzE;AAEA,UAAM,mBAAmB,YAAY,IAAI,CAAC,MAAM;AAC9C,UAAI,EAAE,YAAa,QAAO;AAC1B,YAAM,cAAc,eAAe,EAAE,SAAS;AAC9C,aAAO,cAAc,EAAE,GAAG,GAAG,YAAY,IAAI;AAAA,IAC/C,CAAC;AACD,UAAM,mBAAsE,CAAC;AAC7E,eAAW,QAAQ,KAAK,YAAY;AAClC,UAAI,QAAQ,IAAI,IAAI,EAAG;AACvB,YAAM,cAAc,eAAe,IAAI;AACvC,UAAI,YAAa,kBAAiB,KAAK,EAAE,WAAW,MAAM,YAAY,CAAC;AAAA,IACzE;AACA,UAAM,iBAAiB,CAAC,GAAG,kBAAkB,GAAG,gBAAgB;AAChE,QAAI,eAAe,SAAS,EAAG,GAAE,iBAAiB;AAIlD,QAAI,KAAK,QAAQ,SAAS,OAAO,KAAK,KAAK,OAAO,KAAK,EAAE,SAAS,EAAG,GAAE,QAAQ,KAAK,OAAO;AAC3F,QAAI,KAAK,QAAQ,SAAS,KAAM,GAAE,QAAQ,KAAK,OAAO;AACtD,QAAI,KAAK,QAAQ,UAAU,KAAM,GAAE,SAAS,KAAK,OAAO;AACxD,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,WACZ,UACA,WACA,UACA,YACA,YACA,SACoC;AACpC,UAAM,MAAM,UAAU;AACtB,UAAM,MAAM,UAAU,kBAAkB,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,cAAc,IAAI,SAAS;AACrF,QAAI,CAAC,MAAM,CAAC,GAAG,WAAW;AACxB,YAAM,IAAI;AAAA,QACR,0DAA0D,IAAI,SAAS;AAAA,MACzE;AAAA,IACF;AACA,UAAM,QAA0B,MAAM,QAAQ,GAAG,SAAS,IACtD,CAAC,GAAG,UAAU,CAAC,GAAG,GAAG,UAAU,CAAC,KAAK,GAAG,UAAU,CAAC,CAAC,IACpD,CAAC,GAAG,WAAW,GAAG,SAAS;AAC/B,UAAM,UAAU,WAAW,OAAO,IAAI,IAAI;AAC1C,UAAM,aAAa,UAAU,kBAAkB,CAAC,GAAG;AAAA,MAAI,CAAC,MACtD,EAAE,cAAc,IAAI,YAAY,EAAE,GAAG,GAAG,WAAW,QAAQ,IAAI;AAAA,IACjE;AAeA,UAAM,MAAM,MAAM,KAAK;AAAA,MACrB;AAAA,MACA,EAAE,GAAG,WAAW,gBAAgB,UAAU;AAAA,MAC1C,EAAE,UAAU,YAAY,YAAY,QAAQ;AAAA,IAC9C;AAEA,WAAO,IAAI,KAAK,IAAI,CAAC,QAAQ;AAC3B,YAAM,MAA+B,CAAC;AACtC,iBAAW,OAAO,WAAY,KAAI,GAAG,IAAI,IAAI,GAAG;AAChD,iBAAW,KAAK,SAAU,KAAI,GAAG,CAAC,WAAW,IAAI,IAAI,CAAC;AACtD,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AACF;AAOO,SAAS,kBACd,MACA,OACA,YACA,cAC2B;AAC3B,QAAM,QAAQ,CAAC,QAAiC,WAAW,IAAI,CAAC,MAAM,OAAO,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,GAAG;AACpG,QAAM,QAAQ,oBAAI,IAAqC;AACvD,aAAW,OAAO,KAAM,OAAM,IAAI,MAAM,GAAG,GAAG,GAAG;AAEjD,aAAW,OAAO,OAAO;AACvB,UAAM,MAAM,MAAM,GAAG;AACrB,UAAM,SAAS,MAAM,IAAI,GAAG;AAC5B,QAAI,QAAQ;AACV,iBAAW,KAAK,aAAc,QAAO,CAAC,IAAI,IAAI,CAAC;AAAA,IACjD,OAAO;AACL,YAAM,QAAiC,CAAC;AACxC,iBAAW,KAAK,WAAY,OAAM,CAAC,IAAI,IAAI,CAAC;AAC5C,iBAAW,KAAK,aAAc,OAAM,CAAC,IAAI,IAAI,CAAC;AAC9C,YAAM,IAAI,KAAK,KAAK;AACpB,WAAK,KAAK,KAAK;AAAA,IACjB;AAAA,EACF;AACA,SAAO;AACT;;;AC91BA,IAAM,eAAe,oBAAI,IAAI,CAAC,UAAU,eAAe,CAAC;AA4CjD,SAAS,yBACd,YACA,MACA,MACA,cACA,SACoB;AACpB,QAAM,YAAY,IAAI,IAAI,KAAK,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AACtD,QAAM,UAAU,CAAC,cAAiD;AAChE,UAAM,MAAM,UAAU,IAAI,SAAS;AACnC,WAAO,MAAM,KAAK,gBAAgB,UAAU,IAAI,IAAI,KAAK,IAAI;AAAA,EAC/D;AACA,SAAO;AAAA,IACL,eAAe,WAAW;AACxB,YAAM,OAAO,QAAQ,SAAS;AAC9B,UAAI,CAAC,KAAM,QAAO;AAClB,UAAI,MAAM,QAAQ,KAAK,OAAO,KAAK,KAAK,QAAQ,SAAS,EAAG,QAAO;AACnE,aAAO,CAAC,EAAE,KAAK,QAAQ,aAAa,IAAI,KAAK,IAAI,KAAK,KAAK;AAAA,IAC7D;AAAA,IACA,MAAM,cAAc,WAAW,QAAQ;AACrC,YAAM,OAAO,QAAQ,SAAS;AAC9B,UAAI,CAAC,KAAM,QAAO;AAClB,UAAI,MAAM,QAAQ,KAAK,OAAO,KAAK,KAAK,QAAQ,SAAS,GAAG;AAC1D,cAAM,eAAe,oBAAI,IAAqB;AAC9C,mBAAW,OAAO,KAAK,SAAS;AAC9B,cAAI,OAAO,IAAI,SAAS,KAAM,cAAa,IAAI,IAAI,OAAO,OAAO,IAAI,KAAK,CAAC;AAAA,QAC7E;AACA,eAAO;AAAA,MACT;AACA,UAAI,KAAK,QAAQ,aAAa,IAAI,KAAK,IAAI,KAAK,KAAK,WAAW;AAC9D,YAAI;AACJ,YAAI,cAAc;AAChB,cAAI;AACF,oBAAQ,MAAM,aAAa,KAAK,SAAS;AAAA,UAC3C,QAAQ;AACN,mBAAO;AAAA,UACT;AAAA,QACF;AACA,eAAO,KAAK,kBAAkB,KAAK,WAAW,QAAQ,SAAS,QAAW,OAAO;AAAA,MACnF;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAaO,SAAS,oBAAoB,MAA8C;AAGhF,QAAM,QAAQ,oBAAI,IAAyC;AAC3D,SAAO;AAAA,IACL,iBAAiB,CAAC,eAAe,KAAK,gBAAgB,UAAU;AAAA,IAChE,MAAM,kBAAkB,cAAc,KAAK,OAAO,SAAS;AACzD,UAAI,QAAQ,MAAM,IAAI,YAAY;AAClC,UAAI,CAAC,OAAO;AACV,gBAAQ,oBAAI,IAAI;AAChB,cAAM,IAAI,cAAc,KAAK;AAAA,MAC/B;AACA,YAAM,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC;AACjD,UAAI,QAAQ,SAAS,GAAG;AACtB,cAAM,UAAU,MAAM,KAAK,kBAAkB,cAAc,SAAS,OAAO,OAAO;AAClF,mBAAW,MAAM,QAAS,OAAM,IAAI,IAAI,QAAQ,IAAI,EAAE,KAAK,IAAI;AAAA,MACjE;AACA,YAAM,MAAM,oBAAI,IAAqB;AACrC,iBAAW,MAAM,KAAK;AACpB,cAAM,QAAQ,MAAM,IAAI,EAAE;AAC1B,YAAI,SAAS,KAAM,KAAI,IAAI,IAAI,KAAK;AAAA,MACtC;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAKA,IAAM,MAAM,CAAC,MAAc,OAAO,CAAC,EAAE,SAAS,GAAG,GAAG;AAmB7C,SAAS,iBAAiB,OAAgB,aAAiD;AAChG,MAAI,SAAS,QAAQ,iBAAiB,SAAS,OAAO;AACpD,QAAI,OAAO,UAAU,YAAY,OAAO,UAAU,SAAU,QAAO;AAAA,EACrE;AAQA,MAAI,gBAAgB,QAAQ;AAC1B,UAAMC,KAAI,OAAO,UAAU,WAAW,QAAQ,OAAO,OAAO,KAAK,EAAE,KAAK,CAAC;AACzE,QAAI,OAAO,UAAUA,EAAC,KAAKA,MAAK,OAAQA,MAAK,KAAM,QAAO,OAAOA,EAAC;AAAA,EACpE;AACA,MAAI;AACJ,MAAI,iBAAiB,KAAM,KAAI;AAAA,WACtB,OAAO,UAAU,SAAU,KAAI,IAAI,KAAK,KAAK;AAAA,OACjD;AACH,UAAM,IAAI,OAAO,KAAK,EAAE,KAAK;AAE7B,QAAI,QAAQ,KAAK,CAAC,IAAI,IAAI,KAAK,OAAO,CAAC,IAAI,OAAO,OAAO,CAAC,IAAI,MAAO,OAAO,CAAC,CAAC,IAAI,IAAI,KAAK,CAAC;AAAA,EAC9F;AACA,MAAI,OAAO,MAAM,EAAE,QAAQ,CAAC,EAAG,QAAO;AACtC,QAAM,IAAI,EAAE,eAAe;AAC3B,QAAM,IAAI,EAAE,YAAY;AACxB,UAAQ,aAAa;AAAA,IACnB,KAAK;AAAQ,aAAO,OAAO,CAAC;AAAA,IAC5B,KAAK;AAAW,aAAO,GAAG,CAAC,KAAK,KAAK,MAAM,IAAI,CAAC,IAAI,CAAC;AAAA,IACrD,KAAK;AAAS,aAAO,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC;AAAA,IACvC,KAAK;AAAA,IACL,KAAK;AAAA,IACL;AAAS,aAAO,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,IAAI,IAAI,EAAE,WAAW,CAAC,CAAC;AAAA,EAC3D;AACF;AAoBA,eAAsB,uBACpB,YACA,MACA,MACA,MACA,cACA,SACe;AACf,MAAI,CAAC,KAAK,UAAU,CAAC,KAAK,OAAQ;AAClC,QAAM,SAAS,KAAK,gBAAgB,UAAU;AAC9C,MAAI,CAAC,OAAQ;AAEb,aAAW,OAAO,MAAM;AACtB,UAAM,OAAO,OAAO,IAAI,KAAK;AAK7B,QAAI,IAAI,SAAS,UAAW,QAAQ,KAAK,SAAS,QAAS;AACzD,iBAAW,OAAO,MAAM;AACtB,cAAM,YAAY,iBAAiB,IAAI,IAAI,IAAI,GAAG,IAAI,eAAe;AACrE,YAAI,aAAa,KAAM,KAAI,IAAI,IAAI,IAAI;AAAA,MACzC;AACA;AAAA,IACF;AAEA,QAAI,CAAC,KAAM;AAGX,QAAI,MAAM,QAAQ,KAAK,OAAO,KAAK,KAAK,QAAQ,SAAS,GAAG;AAC1D,YAAM,eAAe,oBAAI,IAAqB;AAC9C,iBAAW,OAAO,KAAK,SAAS;AAC9B,YAAI,OAAO,IAAI,SAAS,KAAM,cAAa,IAAI,IAAI,OAAO,OAAO,IAAI,KAAK,CAAC;AAAA,MAC7E;AACA,UAAI,aAAa,SAAS,EAAG;AAC7B,iBAAW,OAAO,MAAM;AACtB,cAAM,MAAM,IAAI,IAAI,IAAI;AACxB,cAAM,QAAQ,aAAa,IAAI,GAAG;AAClC,YAAI,SAAS,KAAM,KAAI,IAAI,IAAI,IAAI;AAAA,MACrC;AACA;AAAA,IACF;AAGA,QAAI,KAAK,QAAQ,aAAa,IAAI,KAAK,IAAI,KAAK,KAAK,WAAW;AAC9D,YAAM,MAAM,MAAM;AAAA,QAChB,IAAI,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE,IAAI,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,KAAK,IAAI,CAAC;AAAA,MAC/D;AACA,UAAI,IAAI,WAAW,EAAG;AAMtB,UAAI;AACJ,UAAI,cAAc;AAChB,YAAI;AACF,kBAAQ,MAAM,aAAa,KAAK,SAAS;AAAA,QAC3C,QAAQ;AACN;AAAA,QACF;AAAA,MACF;AACA,YAAM,YAAY,MAAM,KAAK,kBAAkB,KAAK,WAAW,KAAK,SAAS,QAAW,OAAO;AAC/F,UAAI,CAAC,aAAa,UAAU,SAAS,EAAG;AACxC,iBAAW,OAAO,MAAM;AACtB,cAAM,QAAQ,UAAU,IAAI,IAAI,IAAI,IAAI,CAAC;AACzC,YAAI,SAAS,KAAM,KAAI,IAAI,IAAI,IAAI;AAAA,MACrC;AAAA,IACF;AAAA,EACF;AACF;AAOO,SAAS,iBACd,QACoB;AACpB,MAAI,CAAC,OAAQ,QAAO;AACpB,aAAW,aAAa,CAAC,QAAQ,SAAS,OAAO,GAAG;AAClD,QAAI,OAAO,SAAS,EAAG,QAAO;AAAA,EAChC;AACA,aAAW,CAAC,MAAM,IAAI,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,QAAI,KAAK,SAAS,UAAU,KAAK,SAAS,SAAU,QAAO;AAAA,EAC7D;AACA,SAAO;AACT;;;AC/VA,SAAS,wBAAwB,sBAAAC,qBAAoB,oBAAoB;AA6BzE,SAAS,QAAQ,GAAY,GAAoB;AAC/C,MAAI,OAAO,MAAM,YAAY,OAAO,MAAM,SAAU,QAAO,IAAI;AAC/D,MAAI,aAAa,QAAQ,aAAa,MAAM;AAC1C,UAAM,KAAK,aAAa,CAAC;AACzB,UAAM,KAAK,aAAa,CAAC;AACzB,QAAI,OAAO,QAAQ,OAAO,KAAM,QAAO,KAAK;AAAA,EAC9C;AACA,SAAO,OAAO,CAAC,IAAI,OAAO,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI,OAAO,CAAC,IAAI,IAAI;AAClE;AAWA,SAAS,SAAS,OAAgB,OAAyB;AACzD,QAAM,UAAUA,oBAAmB,KAAK;AACxC,MAAI,WAAW,KAAM,QAAO,QAAQ,OAAO,OAAO,IAAI;AACtD,SAAO,QAAQ,OAAO,KAAK,KAAK;AAClC;AAEA,SAAS,QAAQ,OAAgB,IAAY,UAA4B;AACvE,UAAQ,IAAI;AAAA,IACV,KAAK;AAAO,aAAO,UAAU,YAAY,OAAO,KAAK,MAAM,OAAO,QAAQ;AAAA,IAC1E,KAAK;AAAO,aAAO,EAAE,UAAU,YAAY,OAAO,KAAK,MAAM,OAAO,QAAQ;AAAA,IAC5E,KAAK;AAAO,aAAO,SAAS,QAAQ,QAAQ,OAAO,QAAQ,IAAI;AAAA,IAC/D,KAAK;AAAQ,aAAO,SAAS,QAAQ,QAAQ,OAAO,QAAQ,KAAK;AAAA,IACjE,KAAK;AAAO,aAAO,SAAS,QAAQ,QAAQ,OAAO,QAAQ,IAAI;AAAA,IAC/D,KAAK,QAAQ;AACX,UAAI,SAAS,KAAM,QAAO;AAM1B,aAAO,SAAS,OAAO,QAAQ;AAAA,IACjC;AAAA,IACA,KAAK,YAAY;AAKf,UAAI,SAAS,QAAQ,CAAC,MAAM,QAAQ,QAAQ,KAAK,SAAS,WAAW,EAAG,QAAO;AAC/E,YAAM,CAAC,KAAK,GAAG,IAAI;AACnB,UAAI,OAAO,QAAQ,OAAO,KAAM,QAAO;AACvC,aAAO,QAAQ,OAAO,GAAG,KAAK,KAAK,SAAS,OAAO,GAAG;AAAA,IACxD;AAAA,IACA,KAAK;AAAO,aAAO,MAAM,QAAQ,QAAQ,KAAK,SAAS,KAAK,CAAC,MAAM,UAAU,KAAK,OAAO,KAAK,MAAM,OAAO,CAAC,CAAC;AAAA,IAC7G,KAAK;AAAQ,aAAO,MAAM,QAAQ,QAAQ,KAAK,CAAC,SAAS,KAAK,CAAC,MAAM,UAAU,KAAK,OAAO,KAAK,MAAM,OAAO,CAAC,CAAC;AAAA,IAC/G,KAAK;AAAa,aAAO,OAAO,SAAS,EAAE,EAAE,YAAY,EAAE,SAAS,OAAO,YAAY,EAAE,EAAE,YAAY,CAAC;AAAA,IACxG;AAAS,aAAO;AAAA,EAClB;AACF;AAEO,SAAS,aAAa,KAAU,OAAqD;AAC1F,MAAI,CAAC,MAAO,QAAO;AACnB,aAAW,CAAC,KAAK,IAAI,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC/C,QAAI,QAAQ,QAAQ;AAClB,UAAI,CAAE,KAAe,MAAM,CAAC,MAAM,aAAa,KAAK,CAAQ,CAAC,EAAG,QAAO;AAAA,IACzE,WAAW,QAAQ,OAAO;AACxB,UAAI,CAAE,KAAe,KAAK,CAAC,MAAM,aAAa,KAAK,CAAQ,CAAC,EAAG,QAAO;AAAA,IACxE,WAAW,QAAQ,QAAQ;AACzB,UAAI,aAAa,KAAK,IAAW,EAAG,QAAO;AAAA,IAC7C,WAAW,SAAS,QAAQ,OAAO,SAAS,YAAY,CAAC,MAAM,QAAQ,IAAI,GAAG;AAC5E,iBAAW,CAAC,IAAI,QAAQ,KAAK,OAAO,QAAQ,IAAW,GAAG;AACxD,YAAI,CAAC,QAAQ,IAAI,GAAG,GAAG,IAAI,QAAQ,EAAG,QAAO;AAAA,MAC/C;AAAA,IACF,WAAW,EAAE,IAAI,GAAG,MAAM,QAAQ,OAAO,IAAI,GAAG,CAAC,MAAM,OAAO,IAAI,IAAI;AACpE,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAIO,SAAS,WAAW,OAAgB,aAAqB,UAAkC;AAChG,QAAM,IAAI,IAAI,KAAK,OAAO,KAAK,CAAC;AAChC,MAAI,OAAO,MAAM,EAAE,QAAQ,CAAC,EAAG,QAAO;AAItC,QAAM,EAAE,MAAM,GAAG,OAAO,KAAK,OAAO,IAAI,uBAAuB,GAAG,QAAQ;AAC1E,QAAM,IAAI,GAAG,KAAK,GAAG,SAAS,GAAG,GAAG;AACpC,QAAM,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,GAAG;AACvC,UAAQ,aAAa;AAAA,IACnB,KAAK;AAAQ,aAAO,GAAG,CAAC;AAAA,IACxB,KAAK;AAAW,aAAO,GAAG,CAAC,KAAK,KAAK,OAAO,QAAQ,KAAK,CAAC,IAAI,CAAC;AAAA,IAC/D,KAAK;AAAS,aAAO,GAAG,CAAC,IAAI,CAAC;AAAA,IAC9B,KAAK,QAAQ;AAEX,YAAM,SAAS,IAAI,KAAK,KAAK,IAAI,GAAG,QAAQ,GAAG,MAAM,CAAC;AACtD,YAAM,OAAO,OAAO,UAAU,IAAI,KAAK;AACvC,aAAO,WAAW,OAAO,WAAW,IAAI,GAAG;AAC3C,aAAO,OAAO,YAAY,EAAE,MAAM,GAAG,EAAE;AAAA,IACzC;AAAA,IACA,KAAK;AAAA,IACL;AACE,aAAO,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG;AAAA,EAC3B;AACF;AAIA,SAAS,UAAU,MAAa,YAAoB,OAAuB;AACzE,MAAI,eAAe,WAAW,UAAU,KAAK;AAC3C,QAAI,eAAe,iBAAiB;AAClC,aAAO,IAAI,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,MAAM,KAAK,IAAI,CAAC,EAAE;AAAA,IACrE;AACA,WAAO,KAAK;AAAA,EACd;AACA,QAAM,OAAO,KAAK,IAAI,CAAC,MAAM,OAAO,EAAE,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,OAAO,SAAS,CAAC,CAAC;AAC/E,UAAQ,YAAY;AAAA,IAClB,KAAK;AAAiB,aAAO,IAAI,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,MAAM,KAAK,IAAI,CAAC,EAAE;AAAA,IACzF,KAAK;AAAO,aAAO,KAAK,OAAO,CAAC,GAAG,MAAM,IAAI,GAAG,CAAC;AAAA,IACjD,KAAK;AAAO,aAAO,KAAK,SAAS,KAAK,OAAO,CAAC,GAAG,MAAM,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS;AAAA,IACjF,KAAK;AAAO,aAAO,KAAK,SAAS,KAAK,IAAI,GAAG,IAAI,IAAI;AAAA,IACrD,KAAK;AAAO,aAAO,KAAK,SAAS,KAAK,IAAI,GAAG,IAAI,IAAI;AAAA,IACrD;AAAS,aAAO,KAAK,SAAS,KAAK,OAAO,CAAC,GAAG,MAAM,IAAI,GAAG,CAAC,IAAI,KAAK;AAAA,EACvE;AACF;AAOO,SAAS,+BACd,OACA,MACA,MACiB;AAEjB,MAAI,WAAW,KAAK,OAAO,CAAC,MAAM,aAAa,GAAG,MAAM,KAAK,CAAC;AAC9D,QAAM,WAAW,MAAM,kBAAkB,CAAC;AAC1C,aAAW,MAAM,UAAU;AACzB,UAAM,MAAM,KAAK,aAAa,GAAG,SAAS;AAC1C,UAAM,QAAQ,OAAO,KAAK,OAAO,GAAG,SAAS;AAC7C,QAAI,CAAC,GAAG,UAAW;AACnB,UAAM,CAAC,OAAO,GAAG,IAAI,MAAM,QAAQ,GAAG,SAAS,IAAI,GAAG,YAAY,CAAC,GAAG,WAAW,GAAG,SAAS;AAC7F,eAAW,SAAS,OAAO,CAAC,MAAM;AAChC,YAAM,IAAI,OAAO,EAAE,KAAK,KAAK,EAAE;AAI/B,YAAM,UAAUA,oBAAmB,GAAG;AACtC,YAAM,UAAU,WAAW,OAAO,IAAI,UAAU,KAAK,GAAG,GAAG;AAC3D,aAAO,KAAK,OAAO,KAAK,KAAK;AAAA,IAC/B,CAAC;AAAA,EACH;AAGA,QAAM,aAAa,MAAM,cAAc,CAAC;AACxC,QAAM,WAAW,MAAM;AACvB,QAAM,YAAY,IAAI,IAAI,SAAS,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,WAAY,CAAC,CAAC;AACzG,QAAM,QAAQ,CAAC,MAAyC;AACtD,UAAM,SAAc,CAAC;AACrB,eAAW,QAAQ,YAAY;AAC7B,YAAM,MAAM,KAAK,aAAa,IAAI;AAClC,YAAM,QAAQ,OAAO,KAAK,OAAO,IAAI;AACrC,YAAM,MAAM,EAAE,KAAK;AACnB,YAAM,OAAO,UAAU,IAAI,IAAI,MAAM,KAAK,SAAS,UAAU,IAAI,eAAe,WAAW,IAAI,OAAO,IAAI,cAAc,CAAC,CAAC,IAAI;AAC9H,aAAO,IAAI,IAAI,OAAO,WAAW,KAAK,MAAM,QAAQ,IAAK,OAAO;AAAA,IAClE;AACA,WAAO,EAAE,KAAK,KAAK,UAAU,MAAM,GAAG,OAAO;AAAA,EAC/C;AAEA,QAAM,SAAS,oBAAI,IAA0C;AAC7D,aAAW,KAAK,UAAU;AACxB,UAAM,EAAE,KAAK,OAAO,IAAI,MAAM,CAAC;AAC/B,UAAM,IAAI,OAAO,IAAI,GAAG,KAAK,EAAE,QAAQ,MAAM,CAAC,EAAE;AAChD,MAAE,KAAK,KAAK,CAAC;AACb,WAAO,IAAI,KAAK,CAAC;AAAA,EACnB;AAEA,MAAI,WAAW,WAAW,KAAK,OAAO,SAAS,GAAG;AAChD,WAAO,IAAI,MAAM,EAAE,QAAQ,CAAC,GAAG,MAAM,CAAC,EAAE,CAAC;AAAA,EAC3C;AAGA,QAAM,MAAa,CAAC;AACpB,aAAW,KAAK,OAAO,OAAO,GAAG;AAC/B,UAAM,MAAW,EAAE,GAAG,EAAE,OAAO;AAC/B,eAAW,KAAK,MAAM,UAAU;AAC9B,YAAM,SAAS,KAAK,WAAW,CAAC;AAChC,UAAI,CAAC,IAAI,UAAU,EAAE,MAAM,OAAO,QAAQ,QAAQ,OAAO,GAAG,OAAO,QAAQ,OAAO,GAAG,CAAC;AAAA,IACxF;AACA,QAAI,KAAK,GAAG;AAAA,EACd;AAGA,aAAW,CAAC,KAAK,GAAG,KAAK,OAAO,QAAQ,MAAM,SAAS,CAAC,CAAC,EAAE,QAAQ,GAAG;AACpE,QAAI,KAAK,CAAC,GAAG,OAAO,QAAQ,SAAS,KAAK,KAAK,QAAQ,EAAE,GAAG,GAAG,EAAE,GAAG,CAAC,CAAC;AAAA,EACxE;AACA,QAAM,SAAS,MAAM,UAAU;AAC/B,QAAM,UAAU,IAAI,MAAM,QAAQ,MAAM,SAAS,OAAO,SAAS,MAAM,QAAQ,MAAS;AAExF,SAAO;AAAA,IACL,MAAM;AAAA,IACN,QAAQ;AAAA,MACN,GAAG,WAAW,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,SAAS,EAAE;AAAA,MACtD,GAAG,MAAM,SAAS,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,SAAS,EAAE;AAAA,IAC5D;AAAA,EACF;AACF;;;AV9KA,SAAS,qBAAqB,KAAuB;AACnD,QAAM,MAAM,OAAQ,KAA+B,WAAW,OAAO,EAAE,EAAE,YAAY;AACrF,SACE,IAAI,SAAS,eAAe;AAAA,EAC3B,IAAI,SAAS,UAAU,KAAK,IAAI,SAAS,gBAAgB;AAAA,EAC1D,IAAI,SAAS,eAAe;AAAA,EAC5B,IAAI,SAAS,gBAAgB;AAAA,EAC7B,IAAI,SAAS,gBAAgB,KAC7B,IAAI,SAAS,4BAA4B;AAE7C;AAQA,IAAM,kBAAkB;AA6LxB,IAAM,uBAAoD;AAAA,EACxD,WAAW;AAAA,EACX,mBAAmB;AAAA,EACnB,UAAU;AACZ;AAoBO,IAAM,mBAAN,MAAoD;AAAA,EAwBzD,YAAY,SAAiC,CAAC,GAAG;AAjBjD;AAAA,SAAiB,kBAAkB,oBAAI,IAA6B;AAapE;AAAA,SAAQ,yBAAyB;AAK/B,SAAK,SAAS,OAAO,UAAU,aAAa,EAAE,OAAO,QAAQ,QAAQ,SAAS,CAAC;AAC/E,SAAK,eAAe,IAAI,aAAa;AAGrC,QAAI,OAAO,OAAO;AAChB,WAAK,aAAa,YAAY,OAAO,KAAK;AAAA,IAC5C;AAEA,SAAK,oBAAoB,OAAO;AAChC,SAAK,uBAAuB,OAAO;AACnC,SAAK,kBAAkB,OAAO;AAC9B,SAAK,gBAAgB,OAAO;AAC5B,SAAK,oBAAoB,OAAO;AAChC,SAAK,qBAAqB,OAAO;AACjC,SAAK,sBAAsB,OAAO;AAGlC,QAAI,OAAO,UAAU;AACnB,iBAAW,MAAM,OAAO,UAAU;AAChC,YAAI;AACF,eAAK,gBAAgB,EAAE;AAAA,QACzB,SAAS,GAAG;AACV,eAAK,QAAQ,OAAO,2CAA2C,IAAI,IAAI,MAAM,OAAQ,GAAa,WAAW,CAAC,CAAC,EAAE;AAAA,QACnH;AAAA,MACF;AAAA,IACF;AAIA,SAAK,UAAU;AAAA,MACb,SAAS,CAAC,SAAS,KAAK,aAAa,IAAI,IAAI;AAAA,MAC7C,mBAAmB,OAAO,sBAAsB,MAAM;AAAA,MACtD,eAAe,OAAO;AAAA,MACtB,kBAAkB,OAAO;AAAA,MACzB,iBAAiB,OAAO;AAAA;AAAA;AAAA,MAGxB,yBAAyB,CAAC,aACxB,KAAK,gBAAgB,IAAI,QAAQ,GAAG,wBACjC,OAAO,0BAA0B,QAAQ;AAAA,MAC9C,2BAA2B,OAAO;AAAA,MAClC,4BAA4B,OAAO;AAAA,MACnC,kBAAkB,OAAO;AAAA,IAC3B;AAMA,UAAM,UAA+B;AAAA,MACnC,IAAI,kBAAkB;AAAA,MACtB,IAAI,iBAAiB;AAAA,IACvB;AAGA,QAAI,OAAO,iBAAiB;AAC1B,cAAQ,KAAK,IAAI,yBAAyB,CAAC;AAAA,IAC7C;AAEA,UAAM,SAAS,OAAO,cAAc,CAAC;AACrC,SAAK,aAAa,CAAC,GAAG,SAAS,GAAG,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;AAEhF,SAAK,OAAO;AAAA,MACV,gCAAgC,KAAK,aAAa,IAAI,WACnD,KAAK,WAAW,MAAM,gBAAgB,KAAK,WAAW,IAAI,OAAK,EAAE,IAAI,EAAE,KAAK,UAAK,CAAC;AAAA,IACvF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,QACZ,OACA,SAC0B;AAM1B,QAAI,CAAC,KAAK,kBAAmB,QAAO,EAAE,GAAG,KAAK,SAAS,QAAQ;AAK/D,UAAM,SAAS,MAAM,KAAK,kBAAkB,OAAO,OAAO;AAC1D,WAAO;AAAA,MACL,GAAG,KAAK;AAAA,MACR;AAAA,MACA,cAAc,CAAC,eAAuB,OAAO,IAAI,UAAU,KAAK;AAAA,IAClE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,MAAc,kBACZ,OACA,SACuC;AACvC,UAAM,MAAM,oBAAI,IAA6B;AAC7C,UAAM,WAAW,KAAK;AACtB,QAAI,CAAC,YAAY,CAAC,MAAM,KAAM,QAAO;AACrC,UAAM,OAAO,KAAK,aAAa,IAAI,MAAM,IAAI;AAC7C,QAAI,CAAC,KAAM,QAAO;AAElB,UAAM,UAAU,oBAAI,IAAY;AAChC,QAAI,OAAO,KAAK,QAAQ,YAAY,KAAK,IAAI,KAAK,GAAG;AACnD,cAAQ,IAAI,KAAK,IAAI,KAAK,CAAC;AAAA,IAC7B;AACA,UAAM,QAAS,KAAuD;AACtE,QAAI,OAAO;AACT,iBAAW,CAAC,OAAO,CAAC,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC9C,gBAAQ,IAAI,GAAG,QAAQ,KAAK;AAAA,MAC9B;AAAA,IACF;AAEA,eAAW,UAAU,SAAS;AAC5B,UAAI;AACJ,UAAI;AACF,iBAAS,MAAM,SAAS,QAAQ,OAAO;AAAA,MACzC,SAAS,GAAG;AAEV,aAAK,OAAO;AAAA,UACV,wDAAwD,MAAM;AAAA,UAE9D,aAAa,QAAQ,IAAI,IAAI,MAAM,OAAO,CAAC,CAAC;AAAA,QAC9C;AACA,cAAM,IAAI;AAAA,UACR,iDAAiD,MAAM;AAAA,QACzD;AAAA,MACF;AACA,UAAI,UAAU,KAAM,KAAI,IAAI,QAAQ,MAAM;AAAA,IAC5C;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM,MAAM,OAAuB,SAAsD;AACvF,QAAI,CAAC,MAAM,MAAM;AACf,YAAM,IAAI,MAAM,0CAA0C;AAAA,IAC5D;AAEA,SAAK,WAAW,KAAK;AACrB,UAAM,MAAM,MAAM,KAAK,QAAQ,OAAO,OAAO;AAC7C,QAAI;AACJ,eAAS;AACP,YAAM,WAAW,KAAK,gBAAgB,OAAO,KAAK,IAAI;AACtD,WAAK,OAAO,MAAM,8BAA8B,MAAM,IAAI,YAAO,SAAS,IAAI,EAAE;AAChF,UAAI;AACF,eAAO,MAAM,SAAS,QAAQ,OAAO,GAAG;AAAA,MAC1C,SAAS,GAAG;AACV,YAAK,GAAyB,SAAS,uBAAuB;AAC5D,eAAK,OAAO;AAAA,YACV,eAAe,SAAS,IAAI;AAAA,UAC9B;AACA,WAAC,gBAAS,oBAAI,IAAI,IAAG,IAAI,QAAQ;AACjC;AAAA,QACF;AACA,cAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,gBAAgB,SAAmC;AACjD,UAAM,WAAW,eAAe,SAAS,KAAK,oBAAoB;AAClE,SAAK,aAAa,SAAS,SAAS,IAAI;AACxC,SAAK,gBAAgB,IAAI,QAAQ,MAAM,QAAQ;AAC/C,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,aACJ,SACA,WACA,SACA,SAC0B;AAC1B,UAAM,WAAW,KAAK,gBAAgB,OAAO;AAC7C,SAAK,OAAO,MAAM,6BAA6B,QAAQ,IAAI,aAAa,QAAQ,MAAM,cAAc,QAAQ,WAAW,CAAC,GAAG,KAAK,GAAG,KAAK,QAAG,GAAG;AAS9I,QAAI,SAAS,iBAAiB,KAAK,mBAAmB;AACpD,UAAI,WAA6C;AACjD,UAAI;AACF,mBAAW,MAAM,KAAK,kBAAkB,QAAQ,QAAQ,OAAO;AAAA,MACjE,SAAS,GAAG;AACV,aAAK,OAAO,KAAK,kDAAkD,QAAQ,MAAM,uCAAkC,OAAQ,GAAa,WAAW,CAAC,CAAC,EAAE;AAAA,MACzJ;AACA,UAAI,UAAU;AACZ,aAAK,OAAO,MAAM,6BAA6B,QAAQ,IAAI,yBAAoB,SAAS,MAAM,sBAAsB;AACpH,cAAM,iBAAiB;AAAA,UACrB,OAAO,OAAO,MAAsB,+BAA+B,GAAG,SAAS,MAAM,QAAS;AAAA,QAChG;AACA,cAAM,gBAAgB,MAAM,IAAI,gBAAgB,cAAc,EAAE,QAAQ,UAAU,WAAW,OAAO;AAGpG,eAAO;AAAA,MACT;AAAA,IACF;AAKA,UAAM,WAAW,KAAK;AACtB,UAAM,eAAe,WACjB,CAAC,iBAAyB,SAAS,cAAc,OAAO,IACxD;AAKJ,UAAM,YAAY,KAAK,gBAAgB,oBAAoB,KAAK,aAAa,IAAI;AAIjF,UAAM,cAAc,aAAa,QAAQ,YAAY,SACjD;AAAA,MACE,QAAQ;AAAA,MACR,QAAQ,WACL,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,EACvB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,OAAO,EAAE,MAAgB,EAAE;AAAA,MAC1D;AAAA,MACA;AAAA,MACA;AAAA,IACF,IACA;AAQJ,QAAI;AACJ,QAAI;AACF,eAAS,MAAM,IAAI,gBAAgB,MAAM,WAAW,EAAE,QAAQ,UAAU,WAAW,OAAO;AAAA,IAC5F,SAAS,KAAK;AACZ,UAAI,qBAAqB,GAAG,GAAG;AAC7B,aAAK,OAAO;AAAA,UACV,wBAAwB,QAAQ,IAAI,qBAAqB,QAAQ,MAAM,qBACnE,OAAQ,KAAe,WAAW,GAAG,CAAC;AAAA,QAC5C;AACA,eAAO,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,QAAQ,CAAC,EAAE;AAAA,MAC5C;AACA,YAAM;AAAA,IACR;AAIA,UAAM,gBAAgB,UAAU,cAAc,CAAC,GAC5C,IAAI,CAAC,SAAS,QAAQ,YAAY,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI,CAAC,EAC9D,OAAO,CAAC,MAAkC,CAAC,CAAC,CAAC;AAWhD,UAAM,YAAY,aAAa,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS,MAAM;AAC3E,QAAI,UAAU,UAAU,OAAO,KAAK,QAAQ;AAC1C,MAAC,OAAoC,SAAS,QAAQ;AACtD,MAAC,OAAoC,kBAAkB,OAAO;AAAA,QAC5D,UAAU,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,KAAe,CAAC;AAAA,MAClD;AACA,MAAC,OAAoC,eAAe,OAAO,KAAK,IAAI,CAAC,QAAQ;AAC3E,cAAM,MAA+B,CAAC;AACtC,mBAAW,KAAK,UAAW,KAAI,EAAE,IAAI,IAAI,IAAI,EAAE,IAAI;AACnD,eAAO;AAAA,MACT,CAAC;AAOD,UAAI,OAAO,QAAQ,QAAQ;AACzB,QAAC,OAAoC,iBAAiB,OAAO,OAAO,IAAI,CAAC,UAAU;AACjF,gBAAM,eAAe,UAAU,OAAO,CAAC,MAAM,MAAM,WAAW,SAAS,EAAE,IAAI,CAAC;AAC9E,iBAAO,MAAM,KAAK,IAAI,CAAC,QAAQ;AAC7B,kBAAM,MAA+B,CAAC;AACtC,uBAAW,KAAK,aAAc,KAAI,EAAE,IAAI,IAAI,IAAI,EAAE,IAAI;AACtD,mBAAO;AAAA,UACT,CAAC;AAAA,QACH,CAAC;AAAA,MACH;AAAA,IACF;AAUA,UAAM,UAAU,UAAU,YAAY,SAAS,YAAY;AAgB3D,UAAM,YAA8G,CAAC;AACrH,eAAW,KAAK,cAAc;AAC5B,UAAI,CAAC,EAAE,SAAS,EAAE,SAAS,OAAQ;AACnC,YAAM,cAAc,4BAA4B,WAAW,EAAE,MAAM,EAAE,eAAe;AACpF,UAAI,CAAC,YAAa;AAClB,YAAM,QAAQ,KAAK,kBAAkB,QAAQ,QAAQ,EAAE,KAAe,GAAG;AACzE,UAAI,UAAU,WAAY,WAAU,KAAK,EAAE,GAAG,aAAa,SAAS,KAAK,CAAC;AAAA,eACjE,UAAU,OAAQ,WAAU,KAAK,EAAE,GAAG,aAAa,SAAS,MAAM,CAAC;AAAA,eACnE,YAAY,MAAO,WAAU,KAAK,EAAE,GAAG,aAAa,SAAS,MAAM,CAAC;AAAA,IAE/E;AACA,QAAI,UAAU,UAAU,OAAO,KAAK,QAAQ;AAC1C,YAAM,QAAQ,CAAC,KAAa,YAC1B,UAAU,IAAI,KAAK,sBAAsB,KAAK,OAAO,CAAC,EAAE,YAAY,IAAI;AAC1E,MAAC,OAAoC,cAAc,OAAO,KAAK,IAAI,CAAC,QAAQ;AAC1E,cAAM,SAAqE,CAAC;AAC5E,mBAAW,EAAE,GAAG,aAAa,QAAQ,KAAK,WAAW;AAGnD,gBAAM,MAAM,yBAAyB,IAAI,EAAE,IAAI,GAAoB,WAAW;AAC9E,cAAI,KAAK;AACP,mBAAO,EAAE,IAAI,IAAI,EAAE,OAAO,EAAE,OAAiB,KAAK,MAAM,IAAI,OAAO,OAAO,GAAG,IAAI,MAAM,IAAI,KAAK,OAAO,EAAE;AAAA,UAC3G;AAAA,QACF;AACA,eAAO;AAAA,MACT,CAAC;AAID,MAAC,OAAoC,SAAS,QAAQ;AAAA,IACxD;AAMA,QAAI,aAAa,aAAa,QAAQ;AAKpC,YAAM,OAAO,aACV,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,EACvB,IAAI,CAAC,OAAO;AAAA,QACX,MAAM,EAAE;AAAA,QACR,OAAO,EAAE;AAAA,QACT,MAAM,EAAE;AAAA,QACR,iBAAiB,4BAA4B,WAAW,EAAE,MAAM,EAAE,eAAe;AAAA,MACnF,EAAE;AACJ,UAAI,KAAK,QAAQ;AAOf,YAAI;AAIF,gBAAM,uBAAuB,QAAQ,QAAQ,MAAM,OAAO,MAAM,WAAW,cAAc,OAAO;AAGhG,qBAAW,SAAS,OAAO,UAAU,CAAC,GAAG;AACvC,kBAAM,SAAS,KAAK,OAAO,CAAC,MAAM,MAAM,WAAW,SAAS,EAAE,IAAI,CAAC;AACnE,gBAAI,OAAO,QAAQ;AACjB,oBAAM,uBAAuB,QAAQ,QAAQ,QAAQ,MAAM,MAAM,WAAW,cAAc,OAAO;AAAA,YACnG;AAAA,UACF;AAAA,QACF,SAAS,GAAG;AACV,eAAK,QAAQ,OAAO,sDAAsD,QAAQ,IAAI,MAAM,OAAQ,GAAa,WAAW,CAAC,CAAC,EAAE;AAAA,QAClI;AAAA,MACF;AAAA,IACF;AAMA,QAAI,OAAO,QAAQ,UAAU,QAAQ,UAAU,QAAQ;AACrD,YAAM,gBAAgB,IAAI,IAAI,QAAQ,SAAS,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AACtE,iBAAW,KAAK,OAAO,QAAQ;AAC7B,cAAM,IAAI,cAAc,IAAI,EAAE,IAAI,KAAK,cAAc,IAAI,EAAE,KAAK,QAAQ,cAAc,EAAE,CAAC;AACzF,YAAI,CAAC,EAAG;AACR,YAAI,EAAE,SAAS,QAAQ,OAAO,EAAE,UAAU,SAAU,GAAE,QAAQ,EAAE;AAChE,YAAI,EAAE,UAAU,QAAQ,EAAE,OAAQ,GAAE,SAAS,EAAE;AAO/C,cAAM,KAAK;AACX,cAAM,KAAK;AACX,cAAM,OAAO,EAAE,QAAQ,KAAK,kBAAkB,QAAQ,QAAQ,EAAE,KAAK,IAAI;AACzE,YAAI,GAAG,YAAY,MAAM;AACvB,gBAAM,WAAW,CAAC,CAAC,GAAG,YAAY,MAAM,SAAS;AACjD,cAAI,UAAU;AACZ,kBAAM,WAAW,GAAG,YAAY,MAAM,mBAAmB,SAAS;AAClE,gBAAI,SAAU,IAAG,WAAW;AAAA,UAC9B;AAAA,QACF;AAQA,YAAI,EAAE,gBAAgB,MAAM;AAC1B,YAAE,eAAe,EAAE,SAAS,OAAO,UAAU,aAAa,eAAe,IAAI;AAAA,QAC/E;AAAA,MACF;AAAA,IACF;AAMA,QAAI,OAAO,QAAQ,UAAU,aAAa,QAAQ;AAChD,YAAM,YAAY,IAAI,IAAI,aAAa,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AAC9D,YAAM,aAAa,IAAI,IAAI,aAAa,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,OAAiB,CAAC,CAAC,CAAC;AACnG,iBAAW,KAAK,OAAO,QAAQ;AAC7B,YAAI,EAAE,SAAS,KAAM;AAGrB,cAAM,IAAI,UAAU,IAAI,EAAE,IAAI,KAAK,WAAW,IAAI,EAAE,IAAI;AACxD,YAAI,KAAK,OAAO,EAAE,UAAU,SAAU,GAAE,QAAQ,EAAE;AAAA,MACpD;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAQ,UAAwC;AAEpD,UAAM,QAAQ,WACV,CAAC,KAAK,aAAa,IAAI,QAAQ,CAAC,EAAE,OAAO,OAAO,IAChD,KAAK,aAAa,OAAO;AAE7B,WAAO,MAAM,IAAI,WAAS;AAAA,MACxB,MAAM,KAAK;AAAA,MACX,OAAO,KAAK;AAAA,MACZ,UAAU,OAAO,QAAQ,KAAK,QAAQ,EAAE,IAAI,CAAC,CAAC,KAAK,OAAO,OAAO;AAAA,QAC/D,MAAM,GAAG,KAAK,IAAI,IAAI,GAAG;AAAA,QACzB,MAAM,QAAQ;AAAA,QACd,OAAO,QAAQ;AAAA,MACjB,EAAE;AAAA,MACF,YAAY,OAAO,QAAQ,KAAK,UAAU,EAAE,IAAI,CAAC,CAAC,KAAK,SAAS,OAAO;AAAA,QACrE,MAAM,GAAG,KAAK,IAAI,IAAI,GAAG;AAAA,QACzB,MAAM,UAAU;AAAA,QAChB,OAAO,UAAU;AAAA,MACnB,EAAE;AAAA,IACJ,EAAE;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YAAY,OAAuB,SAAyE;AAChH,QAAI,CAAC,MAAM,MAAM;AACf,YAAM,IAAI,MAAM,0CAA0C;AAAA,IAC5D;AAEA,SAAK,WAAW,KAAK;AACrB,UAAM,MAAM,MAAM,KAAK,QAAQ,OAAO,OAAO;AAC7C,UAAM,WAAW,KAAK,gBAAgB,OAAO,GAAG;AAChD,SAAK,OAAO,MAAM,oCAAoC,MAAM,IAAI,YAAO,SAAS,IAAI,EAAE;AAEtF,WAAO,SAAS,YAAY,OAAO,GAAG;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBQ,WAAW,OAA6B;AAC9C,UAAM,OAAO,MAAM;AACnB,QAAI,OAAO,KAAK,aAAa,IAAI,IAAI;AAErC,QAAI,CAAC,MAAM;AAOT,WAAK,oBAAoB,IAAI;AAC7B,aAAO,KAAK,mBAAmB,KAAK;AAKpC,WAAK,oBAAoB,OAAO,MAAM,OAAO,KAAK,KAAK,QAAQ,CAAC;AAChE,WAAK,aAAa,SAAS,IAAI;AAO/B,YAAM,kBACH,MAAM,YAAY,UAAU,OAAO,MAAM,MAAM,gBAAgB,UAAU,OAAO;AACnF,YAAM,UACJ,uCAAuC,IAAI,yCAClC,IAAI,eAAe,OAAO,KAAK,KAAK,QAAQ,EAAE,KAAK,GAAG,KAAK,QAAQ,gBAC9D,OAAO,KAAK,KAAK,UAAU,EAAE,KAAK,GAAG,KAAK,QAAQ;AAElE,UAAI,eAAgB,MAAK,OAAO,MAAM,OAAO;AAAA,UACxC,MAAK,OAAO,KAAK,OAAO;AAC7B;AAAA,IACF;AAKA,UAAM,cAAc,CAAC,MAAe,EAAE,SAAS,GAAG,IAAI,EAAE,MAAM,GAAG,EAAE,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI;AACxF,UAAM,gBAAqC,CAAC;AAC5C,eAAW,KAAK,MAAM,YAAY,CAAC,GAAG;AACpC,YAAM,MAAM,YAAY,CAAC;AACzB,UAAI,KAAK,SAAS,GAAG,KAAK,cAAc,GAAG,EAAG;AAC9C,oBAAc,GAAG,IAAI,aAAa,GAAG;AAAA,IACvC;AACA,QAAI,OAAO,KAAK,aAAa,EAAE,SAAS,GAAG;AACzC,YAAM,YAAkB;AAAA,QACtB,GAAG;AAAA,QACH,UAAU,EAAE,GAAG,KAAK,UAAU,GAAG,cAAc;AAAA,MACjD;AAMA,WAAK,oBAAoB,OAAO,WAAW,OAAO,KAAK,KAAK,QAAQ,CAAC;AACrE,WAAK,aAAa,SAAS,SAAS;AACpC,WAAK,OAAO;AAAA,QACV,+BAA+B,IAAI,6BAA6B,OAAO,KAAK,aAAa,EAAE,KAAK,GAAG,CAAC;AAAA,MACtG;AAAA,IACF,OAAO;AAGL,WAAK,oBAAoB,OAAO,MAAM,OAAO,KAAK,KAAK,QAAQ,CAAC;AAAA,IAClE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA8BQ,oBAAoB,OAAuB,MAAY,kBAAkC;AAC/F,UAAM,QAAQ,KAAK;AACnB,QAAI,CAAC,MAAO;AACZ,UAAM,WAAW,MAAM,YAAY,CAAC;AACpC,QAAI,SAAS,WAAW,EAAG;AAE3B,UAAM,SAAS,OAAO,KAAK,QAAQ,WAAW,KAAK,IAAI,KAAK,IAAI;AAChE,QAAI,CAAC,UAAU,CAAC,gBAAgB,KAAK,MAAM,EAAG;AAC9C,UAAM,aAAa,MAAM,MAAM;AAC/B,QAAI,CAAC,cAAc,WAAW,WAAW,EAAG;AAC5C,UAAM,QAAQ,oBAAI,IAAY,CAAC,GAAG,YAAY,MAAM,cAAc,YAAY,CAAC;AAE/E,UAAM,cAAc,CAAC,MAAe,EAAE,SAAS,GAAG,IAAI,EAAE,MAAM,GAAG,EAAE,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI;AAExF,UAAM,gBAAgB,CAAC,YAAmC;AACxD,YAAM,SAAS,KAAK,SAAS,YAAY,OAAO,CAAC;AACjD,UAAI,CAAC,OAAQ,QAAO;AAEpB,UAAI,OAAO,SAAS,YAAY,OAAO,QAAQ,OAAO,OAAO,OAAO,MAAO,QAAO;AAClF,YAAM,SAAS,OAAO,OAAO,QAAQ,WAAW,OAAO,IAAI,KAAK,IAAI;AACpE,UAAI,CAAC,UAAU,WAAW,OAAO,CAAC,gBAAgB,KAAK,MAAM,EAAG,QAAO;AACvE,aAAO;AAAA,IACT;AAOA,UAAM,UAAU,oBAAI,IAAY;AAChC,eAAW,WAAW,UAAU;AAC9B,YAAM,SAAS,cAAc,OAAO;AACpC,UAAI,UAAU,CAAC,MAAM,IAAI,MAAM,EAAG,SAAQ,IAAI,YAAY,OAAO,CAAC;AAAA,IACpE;AACA,QAAI,QAAQ,SAAS,EAAG;AACxB,UAAM,SAAS,iBAAiB,OAAO,CAAC,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC;AAE7D,eAAW,WAAW,UAAU;AAC9B,YAAM,SAAS,cAAc,OAAO;AACpC,UAAI,CAAC,UAAU,MAAM,IAAI,MAAM,EAAG;AAElC,YAAM,MAAM,IAAI;AAAA,QACd,YAAY,OAAO,cAAc,KAAK,IAAI,uBAAuB,MAAM,oBACjE,MAAM,oCACS,OAAO,KAAK,IAAI,KAAK,QAAQ,yJAG5C,MAAM,0BAAqB,CAAC,GAAG,UAAU,EAAE,KAAK,EAAE,KAAK,IAAI,CAAC;AAAA,MACpE;AACA,UAAI,OAAO;AACX,UAAI,SAAS;AACb,UAAI,QAAQ;AACZ,UAAI,SAAS;AACb,UAAI,QAAQ;AACZ,UAAI,UAAU;AACd,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeQ,oBAAoB,MAAoB;AAC9C,UAAM,qBAAqB,KAAK;AAChC,QAAI,CAAC,oBAAoB;AACvB,UAAI,CAAC,KAAK,wBAAwB;AAChC,aAAK,yBAAyB;AAC9B,aAAK,OAAO;AAAA,UACV;AAAA,QAGF;AAAA,MACF;AACA;AAAA,IACF;AACA,QAAI,mBAAmB,IAAI,EAAG;AAC9B,UAAM,MAAM,IAAI;AAAA,MACd,SAAS,IAAI;AAAA,IAGf;AACA,QAAI,OAAO;AACX,QAAI,SAAS;AACb,QAAI,OAAO;AACX,UAAM;AAAA,EACR;AAAA;AAAA,EAGQ,mBAAmB,OAA6B;AACtD,UAAM,WAAW,MAAM;AACvB,UAAM,WAAgC,CAAC;AACvC,UAAM,aAAkC,CAAC;AAEzC,UAAM,cAAc,CAAC,MAAe,EAAE,SAAS,GAAG,IAAI,EAAE,MAAM,GAAG,EAAE,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI;AAGxF,aAAS,QAAQ,EAAE,MAAM,SAAS,OAAO,SAAS,MAAM,SAAS,KAAK,IAAI;AAE1E,eAAW,KAAK,MAAM,YAAY,CAAC,GAAG;AACpC,YAAM,MAAM,YAAY,CAAC;AACzB,UAAI,SAAS,GAAG,EAAG;AACnB,YAAM,WAAW,aAAa,GAAG;AACjC,eAAS,GAAG,IAAI;AAAA,IAClB;AAEA,eAAW,KAAK,MAAM,cAAc,CAAC,GAAG;AACtC,YAAM,MAAM,YAAY,CAAC;AACzB,UAAI,WAAW,GAAG,EAAG;AACrB,iBAAW,GAAG,IAAI,EAAE,MAAM,KAAK,OAAO,KAAK,MAAM,UAAU,KAAK,IAAI;AAAA,IACtE;AAEA,QAAI,MAAM,SAAS,OAAO,MAAM,UAAU,YAAY,CAAC,MAAM,QAAQ,MAAM,KAAK,GAAG;AAIjF,iBAAW,OAAO,OAAO,KAAK,MAAM,KAAgC,GAAG;AACrE,YAAI,IAAI,WAAW,GAAG,EAAG;AACzB,cAAM,WAAW,YAAY,GAAG;AAChC,YAAI,WAAW,QAAQ,KAAK,SAAS,QAAQ,EAAG;AAChD,mBAAW,QAAQ,IAAI,EAAE,MAAM,UAAU,OAAO,UAAU,MAAM,UAAU,KAAK,SAAS;AAAA,MAC1F;AAAA,IACF;AAEA,eAAW,MAAM,MAAM,kBAAkB,CAAC,GAAG;AAC3C,YAAM,MAAM,YAAY,GAAG,SAAS;AACpC,UAAI,WAAW,GAAG,EAAG;AACrB,iBAAW,GAAG,IAAI;AAAA,QAChB,MAAM;AAAA,QAAK,OAAO;AAAA,QAAK,MAAM;AAAA,QAAQ,KAAK;AAAA,QAC1C,eAAe,CAAC,OAAO,QAAQ,SAAS,WAAW,MAAM;AAAA,MAC3D;AAAA,IACF;AAEA,WAAO;AAAA,MACL,MAAM;AAAA,MACN,OAAO;AAAA,MACP,KAAK;AAAA,MACL;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,IACV;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,gBACN,OACA,KACA,MACmB;AACnB,eAAW,YAAY,KAAK,YAAY;AACtC,UAAI,MAAM,IAAI,QAAQ,EAAG;AACzB,UAAI,SAAS,UAAU,OAAO,GAAG,GAAG;AAClC,eAAO;AAAA,MACT;AAAA,IACF;AACA,UAAM,IAAI;AAAA,MACR,sDAAsD,MAAM,IAAI,eACpD,KAAK,WAAW,IAAI,OAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC,GAAG,MAAM,OAAO,yBAAyB,CAAC,GAAG,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC,MAAM,EAAE;AAAA,IAEjJ;AAAA,EACF;AACF;AAoBO,SAAS,aAAa,KAA6H;AACxJ,MAAI,QAAQ,SAAS;AACnB,WAAO,EAAE,MAAM,SAAS,OAAO,SAAS,MAAM,SAAS,KAAK,IAAI;AAAA,EAClE;AACA,QAAM,WAA8E;AAAA,IAClF,CAAC,mBAAmB,gBAAgB;AAAA,IACpC,CAAC,QAAQ,KAAK;AAAA,IACd,CAAC,QAAQ,KAAK;AAAA,IACd,CAAC,YAAY,KAAK;AAAA,IAClB,CAAC,QAAQ,KAAK;AAAA,IACd,CAAC,QAAQ,KAAK;AAAA,EAChB;AACA,aAAW,CAAC,QAAQ,IAAI,KAAK,UAAU;AACrC,QAAI,IAAI,SAAS,MAAM,GAAG;AACxB,YAAM,QAAQ,IAAI,MAAM,GAAG,CAAC,OAAO,MAAM,KAAK;AAC9C,aAAO,EAAE,MAAM,KAAK,OAAO,KAAK,MAAM,KAAK,MAAM;AAAA,IACnD;AAAA,EACF;AACA,SAAO,EAAE,MAAM,KAAK,OAAO,KAAK,MAAM,OAAO,KAAK,IAAI;AACxD;AASA,IAAM,2BAAN,MAA4D;AAAA,EAA5D;AACE,SAAS,OAAO;AAChB,SAAS,WAAW;AAAA;AAAA,EAEpB,UAAU,OAAuB,KAA+B;AAC9D,QAAI,CAAC,MAAM,KAAM,QAAO;AACxB,WAAO,CAAC,CAAC,IAAI;AAAA,EACf;AAAA,EAEA,MAAM,QAAQ,OAAuB,KAAgD;AACnF,WAAO,IAAI,gBAAiB,MAAM,KAAK;AAAA,EACzC;AAAA,EAEA,MAAM,YAAY,OAAuB,KAAmE;AAC1G,QAAI,IAAI,iBAAiB,aAAa;AACpC,aAAO,IAAI,gBAAgB,YAAY,KAAK;AAAA,IAC9C;AACA,WAAO;AAAA,MACL,KAAK,uEAAuE,MAAM,IAAI;AAAA,MACtF,QAAQ,CAAC;AAAA,IACX;AAAA,EACF;AACF;;;AW5iCO,IAAM,yBAAN,MAA+C;AAAA,EAsBpD,YAAY,UAAyC,CAAC,GAAG;AArBzD,gBAAO;AAKP;AAAA;AAAA;AAAA;AAAA,4BAAmB,CAAC,WAAW;AAC/B,mBAAU;AACV,gBAAO;AACP,wBAAyB,CAAC;AAQ1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gCAAiC,CAAC,iCAAiC;AAMjE,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,MAAM,KAAK,KAAmC;AAE5C,QAAI;AACJ,QAAI;AACF,YAAM,WAAW,IAAI,WAA8B,WAAW;AAC9D,UAAI,YAAY,OAAO,SAAS,UAAU,YAAY;AACpD,0BAAkB;AAClB,YAAI,OAAO,MAAM,iEAAiE;AAAA,MACpF;AAAA,IACF,QAAQ;AAAA,IAER;AAQA,QAAI,mBAAmB,KAAK,QAAQ;AACpC,QAAI,cAAc;AAClB,QAAI,CAAC,kBAAkB;AACrB,YAAM,mBAAmB,MAAkC;AACzD,YAAI;AACF,gBAAM,MAAM,IAAI,WAA2B,MAAM;AACjD,iBAAO,OAAO,OAAO,IAAI,cAAc,aAAa,MAAM;AAAA,QAC5D,QAAQ;AACN,iBAAO;AAAA,QACT;AAAA,MACF;AAGA,UAAI,CAAC,iBAAiB,GAAG;AACvB,YAAI,OAAO;AAAA,UACT;AAAA,QAEF;AAAA,MACF;AACA,yBAAmB,OAAO,YAAY,EAAE,SAAS,cAAc,QAAQ,UAAU,QAAQ,MAAM;AAC7F,cAAM,SAAS,iBAAiB;AAChC,YAAI,CAAC,QAAQ;AACX,gBAAM,IAAI;AAAA,YACR;AAAA,UAEF;AAAA,QACF;AACA,cAAM,OAAO,MAAM,OAAO,UAAU,YAAY;AAAA,UAC9C,OAAO;AAAA,UACP;AAAA,UACA,cAAc,cAAc,IAAI,CAAC,OAAO;AAAA,YACtC,UAAU,EAAE;AAAA,YACZ,OAAO,EAAE;AAAA,YACT,OAAO,EAAE;AAAA,UACX,EAAE;AAAA;AAAA;AAAA,UAGF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAMA;AAAA,QACF,CAAC;AACD,eAAO;AAAA,MACT;AACA,oBAAc;AAAA,IAChB;AAMA,QAAI,gBAAgB,KAAK,QAAQ;AACjC,QAAI,oBAAoB;AACxB,QAAI,CAAC,eAAe;AAClB,YAAM,iBAAiB,MAAkC;AACvD,YAAI;AACF,gBAAM,MAAM,IAAI,WAA2B,MAAM;AACjD,iBAAO,OAAO,OAAO,IAAI,YAAY,aAAa,MAAM;AAAA,QAC1D,QAAQ;AACN,iBAAO;AAAA,QACT;AAAA,MACF;AAIA,sBAAgB,OAAO,aAAa,KAAK,WAAW;AAClD,cAAM,SAAS,eAAe;AAC9B,YAAI,CAAC,UAAU,CAAC,OAAO,SAAS;AAC9B,gBAAM,IAAI;AAAA,YACR;AAAA,UACF;AAAA,QACF;AAGA,cAAM,UAAU,IAAI,QAAQ,YAAY,GAAG;AAC3C,cAAM,SAAS,MAAM,OAAO,QAAQ,SAAS,EAAE,MAAM,OAAO,CAAC;AAO7D,YAAI,WAAW,QAAQ,WAAW,QAAW;AAC3C,gBAAM,MAAM,IAAI;AAAA,YACd;AAAA,UAGF;AACA,cAAI,OAAO;AACX,gBAAM;AAAA,QACR;AACA,YAAI,MAAM,QAAQ,MAAM,EAAG,QAAO;AAClC,YAAI,OAAO,WAAW,YAAY,UAAW,QAAoC;AAC/E,iBAAQ,OAA+C;AAAA,QACzD;AACA,eAAO,CAAC;AAAA,MACV;AACA,0BAAoB;AAAA,IACtB;AAKA,UAAM,oBAAoB,KAAK,QAAQ,sBACjC,OAAO;AAAA,MACT,WAAW,CAAC,CAAC;AAAA,MACb,mBAAmB,CAAC,CAAC;AAAA,MACrB,UAAU;AAAA,IACZ;AAgBF,QAAI,eAAe,KAAK,QAAQ;AAChC,QAAI,uBAAuB;AAC3B,QAAI,wBAAwB;AAC5B,QAAI,CAAC,cAAc;AACjB,YAAM,cAAc,MAAsC;AACxD,YAAI;AACF,gBAAM,MAAM,IAAI,WAA+B,UAAU;AACzD,iBAAO,OAAO,OAAO,IAAI,kBAAkB,aAAa,MAAM;AAAA,QAChE,QAAQ;AACN,iBAAO;AAAA,QACT;AAAA,MACF;AASA,8BAAwB,CAAC,CAAC,YAAY;AACtC,qBAAe,CAAC,QAAQ,YAAY,YAAY,GAAG,cAAc,QAAQ,OAAO;AAChF,6BAAuB;AAAA,IACzB;AAQA,UAAM,uBAAuB,CAAC,YAAoB,qBAAiD;AACjG,YAAM,UAAU,MAAM;AACpB,YAAI;AACF,gBAAM,MAAM,IAAI,WAA2B,MAAM;AACjD,iBAAO,OAAO,OAAO,IAAI,cAAc,aAAa,MAAM;AAAA,QAC5D,QAAQ;AAAE,iBAAO;AAAA,QAAW;AAAA,MAC9B,GAAG;AACH,YAAM,MAAM,QAAQ,YAAY,UAAU;AAC1C,YAAM,QAAQ,KAAK,SAAS,gBAAgB;AAC5C,UAAI,UAAU,MAAM,SAAS,YAAY,MAAM,SAAS,oBAAoB,MAAM,WAAW;AAC3F,eAAO,MAAM;AAAA,MACf;AAKA,aAAO,SAAS,SAAY;AAAA,IAC9B;AASA,UAAM,aAAa,MAAkC;AACnD,UAAI;AACF,cAAM,MAAM,IAAI,WAA2B,MAAM;AACjD,eAAO,OAAO,OAAO,IAAI,cAAc,aAAa,MAAM;AAAA,MAC5D,QAAQ;AAAE,eAAO;AAAA,MAAW;AAAA,IAC9B;AACA,UAAM,gBAAoC;AAAA,MACxC,iBAAiB,CAAC,eAAe,WAAW,GAAG,YAAY,UAAU,GAAG;AAAA,MACxE,mBAAmB,OAAO,cAAc,KAAK,OAAO,YAAY;AAC9D,cAAM,MAAM,oBAAI,IAAqB;AACrC,cAAM,eAAe,iBAAiB,WAAW,GAAG,YAAY,YAAY,GAAG,MAAM;AACrF,YAAI,CAAC,gBAAgB,CAAC,oBAAoB,IAAI,WAAW,EAAG,QAAO;AAMnE,cAAM,QAAQ;AACd,iBAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK,OAAO;AAM1C,gBAAM,WAAoC,EAAE,IAAI,EAAE,KAAK,IAAI,MAAM,GAAG,IAAI,KAAK,EAAE,EAAE;AACjF,gBAAM,SAAS,QAAQ,EAAE,MAAM,CAAC,UAAU,KAAK,EAAE,IAAI;AAIrD,gBAAM,OAAO,MAAM,iBAAiB,cAAc;AAAA,YAChD,SAAS,CAAC,MAAM,YAAY;AAAA,YAC5B,cAAc,CAAC,EAAE,OAAO,MAAM,QAAQ,SAAS,OAAO,KAAK,CAAC;AAAA,YAC5D;AAAA;AAAA;AAAA;AAAA,YAIA;AAAA,UACF,CAAC;AACD,qBAAW,KAAK,MAAM;AACpB,gBAAI,EAAE,MAAM,QAAQ,EAAE,YAAY,KAAK,KAAM,KAAI,IAAI,EAAE,IAAI,OAAO,EAAE,YAAY,CAAC,CAAC;AAAA,UACpF;AAAA,QACF;AACA,eAAO;AAAA,MACT;AAAA,IACF;AAOA,UAAM,oBAAoB,OAAO,eAAkE;AAKjG,UAAI;AACJ,UAAI;AACF,mBAAW,IAAI,WAAyB,UAAU;AAAA,MACpD,QAAQ;AAAE,eAAO;AAAA,MAAM;AACvB,UAAI,CAAC,UAAU,gBAAgB,CAAC,SAAS,YAAa,QAAO;AAC7D,YAAM,MAAM,MAAM,SAAS,aAAa,EAAE,MAAM,QAAQ,eAAe,KAAK,CAAC,EAAE,MAAM,MAAM,IAAI;AAC/F,YAAM,OAAO,MAAM,QAAQ,GAAG,IAC1B,MACC,OAAO,OAAO,QAAQ,YAAY,MAAM,QAAS,IAA8B,KAAK,IAClF,IAA6B,QAC9B,CAAC;AACP,YAAM,OAAkC,CAAC;AACzC,UAAI,UAAU;AACd,iBAAW,SAAS,MAAM;AACxB,cAAM,OAAS,OAA8B,QAAQ;AACrD,YAAI,CAAC,MAAM,QAAQ,KAAK,WAAW,WAAY;AAG/C,cAAM,QAAQ,MAAM,SAAS,YAAY,EAAE,MAAM,QAAQ,MAAM,KAAK,MAAM,OAAO,QAAQ,CAAC,EAAE,MAAM,MAAM,IAAI;AAC5G,cAAM,YAAa,OAAqD;AACxE,YAAI,CAAC,UAAW;AAChB,kBAAU;AACV,mBAAW,KAAK,MAAM,QAAQ,UAAU,OAAO,IAAI,UAAU,UAAU,CAAC,GAAG;AACzE,cAAI,KAAK,OAAO,MAAM,SAAU,MAAK,KAAK,CAA4B;AAAA,QACxE;AAAA,MACF;AACA,aAAO,UAAU,OAAO;AAAA,IAC1B;AASA,UAAM,4BAA4B,CAChC,YACA,WACA,UACY;AACZ,UAAI;AACF,cAAM,MAAM,IAAI,WAA2B,MAAM;AACjD,cAAM,SAAS,KAAK,qBAAqB,UAAU;AACnD,YAAI,UAAU,OAAO,OAAO,wBAAwB,YAAY;AAC9D,iBAAO,OAAO,oBAAoB,YAAY,WAAW,KAAK;AAAA,QAChE;AAAA,MACF,QAAQ;AAAA,MAGR;AACA,aAAO;AAAA,IACT;AAOA,UAAM,6BAA6B,CACjC,YACA,WACA,cACW;AACX,UAAI;AACF,cAAM,MAAM,IAAI,WAA2B,MAAM;AACjD,cAAM,SAAS,KAAK,qBAAqB,UAAU;AACnD,YAAI,UAAU,OAAO,OAAO,4BAA4B,YAAY;AAClE,iBAAO,OAAO,wBAAwB,YAAY,WAAW,SAAS;AAAA,QACxE;AAAA,MACF,QAAQ;AAAA,MAGR;AACA,aAAO;AAAA,IACT;AAEA,UAAM,SAAiC;AAAA,MACrC,OAAO,KAAK,QAAQ;AAAA,MACpB,QAAQ,IAAI;AAAA,MACZ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,yBAAyB,KAAK,QAAQ;AAAA,MACtC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA;AAAA;AAAA;AAAA,MAIA,iBAAiB,CAAC,QAAgB,UAAkB;AAClD,cAAM,IAAI,WAAW,GAAG,YAAY,MAAM,GAAG,SAAS,KAAK;AAG3D,eAAO,IAAI,EAAE,MAAM,EAAE,MAAM,KAAK,EAAE,KAAK,iBAAiB,EAAE,gBAAgB,gBAAgB,IAAI;AAAA,MAChG;AAAA;AAAA;AAAA;AAAA,MAIA,kBAAkB,CAAC,eAAuB;AACxC,cAAM,MAAM,WAAW,GAAG,YAAY,UAAU;AAChD,eAAO,CAAC,EAAE,OAAO,IAAI,YAAY;AAAA,MACnC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAUA,oBAAoB,CAAC,SAAiB;AACpC,cAAM,SAAS,WAAW;AAC1B,YAAI,CAAC,OAAQ,QAAO;AACpB,eAAO,OAAO,YAAY,IAAI,KAAK;AAAA,MACrC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAOA,qBAAqB,CAAC,eAAuB;AAC3C,cAAM,SAAS,WAAW,GAAG,YAAY,UAAU,GAAG;AACtD,YAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO;AAClD,cAAM,QAAQ,OAAO,KAAK,MAAM;AAChC,eAAO,MAAM,SAAS,IAAI,QAAQ;AAAA,MACpC;AAAA,MACA;AAAA,IACF;AAEA,QAAI,wBAAwB,uBAAuB;AACjD,UAAI,OAAO,KAAK,iFAA4E;AAAA,IAC9F,WAAW,sBAAsB;AAI/B,UAAI,OAAO;AAAA,QACT;AAAA,MAEF;AAAA,IACF,WAAW,CAAC,cAAc;AACxB,UAAI,OAAO;AAAA,QACT;AAAA,MAGF;AAAA,IACF;AAEA,QAAI,aAAa;AACf,UAAI,OAAO,KAAK,+EAA0E;AAAA,IAC5F;AACA,QAAI,mBAAmB;AACrB,UAAI,OAAO,KAAK,oFAA+E;AAAA,IACjG;AAEA,SAAK,UAAU,IAAI,iBAAiB,MAAM;AAG1C,QAAI,iBAAiB;AACnB,UAAI,eAAe,aAAa,KAAK,OAAO;AAAA,IAC9C,OAAO;AACL,UAAI,gBAAgB,aAAa,KAAK,OAAO;AAAA,IAC/C;AAEA,QAAI,KAAK,QAAQ,OAAO;AACtB,UAAI,KAAK,yBAAyB,OAAO,UAAmB;AAC1D,YAAI,OAAO,MAAM,4BAA4B,EAAE,MAAM,CAAC;AAAA,MACxD,CAAC;AAAA,IACH;AAEA,QAAI,OAAO,KAAK,iCAAiC;AAAA,EACnD;AAAA,EAEA,MAAM,MAAM,KAAmC;AAC7C,QAAI,CAAC,KAAK,QAAS;AAGnB,UAAM,IAAI,QAAQ,mBAAmB,KAAK,OAAO;AAEjD,QAAI,OAAO;AAAA,MACT,oCAAoC,KAAK,QAAQ,aAAa,IAAI,WAC/D,KAAK,QAAQ,aAAa,MAAM,EAAE,KAAK,IAAI,KAAK,QAAQ;AAAA,IAC7D;AAAA,EACF;AAAA,EAEA,MAAM,UAAyB;AAC7B,SAAK,UAAU;AAAA,EACjB;AACF;","names":["v","nextUtcCalendarDay","n","nextUtcCalendarDay","aggregate","y","nextUtcCalendarDay"]}
|