From 6d4605f6309f063593e8d14647a60e7c41fc2ae4 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Wed, 29 Apr 2026 23:09:05 +0200 Subject: [PATCH 01/34] =?UTF-8?q?feat(analytics):=20metric=20view=20source?= =?UTF-8?q?=20=E2=80=94=20Phase=201=20walking=20skeleton?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lands the integration spine for UC Metric View consumption inside the analytics plugin: metric.json config + JSON Schema, type-gen pipeline emitting MetricRegistry augmentation (DESCRIBE TABLE EXTENDED ... AS JSON mocked in tests), POST /api/analytics/metric/:key route with zod body validation, basic SQL constructor (SELECT MEASURE() FROM [LIMIT n]), metric: cache-key namespace reserved, useMetricView React hook, and tests at every layer (94 test files, 1825 tests passing). knip.json: removed obsolete entries (**/*.css, json-schema-to-typescript, tailwindcss, tw-animate-css) that the metric-source schema generation + existing CSS Tailwind imports made redundant. Per prd/analytics-metric-view-source and tasks/.../Phase 1. xavier loop iterations 1+2. Co-authored-by: Isaac Signed-off-by: Atila Fassina --- .../dev-playground/config/queries/metric.json | 9 + docs/static/schemas/metric-source.schema.json | 58 ++ knip.json | 6 +- .../hooks/__tests__/use-metric-view.test.ts | 177 ++++++ packages/appkit-ui/src/react/hooks/index.ts | 9 + packages/appkit-ui/src/react/hooks/types.ts | 99 +++ .../src/react/hooks/use-metric-view.ts | 164 +++++ .../appkit/src/plugins/analytics/analytics.ts | 164 +++++ .../appkit/src/plugins/analytics/index.ts | 1 + .../appkit/src/plugins/analytics/metric.ts | 305 ++++++++++ .../plugins/analytics/tests/analytics.test.ts | 11 +- .../plugins/analytics/tests/metric.test.ts | 567 ++++++++++++++++++ .../appkit/src/plugins/analytics/types.ts | 46 ++ packages/appkit/src/type-generator/index.ts | 54 +- .../src/type-generator/metric-registry.ts | 545 +++++++++++++++++ .../metric-registry.test.ts.snap | 41 ++ .../tests/metric-registry.test.ts | 285 +++++++++ .../appkit/src/type-generator/vite-plugin.ts | 14 +- .../src/schemas/metric-source.generated.ts | 43 ++ .../src/schemas/metric-source.schema.json | 58 ++ .../src/schemas/metric-source.schema.test.ts | 108 ++++ tools/generate-schema-types.ts | 86 ++- 22 files changed, 2817 insertions(+), 33 deletions(-) create mode 100644 apps/dev-playground/config/queries/metric.json create mode 100644 docs/static/schemas/metric-source.schema.json create mode 100644 packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts create mode 100644 packages/appkit-ui/src/react/hooks/use-metric-view.ts create mode 100644 packages/appkit/src/plugins/analytics/metric.ts create mode 100644 packages/appkit/src/plugins/analytics/tests/metric.test.ts create mode 100644 packages/appkit/src/type-generator/metric-registry.ts create mode 100644 packages/appkit/src/type-generator/tests/__snapshots__/metric-registry.test.ts.snap create mode 100644 packages/appkit/src/type-generator/tests/metric-registry.test.ts create mode 100644 packages/shared/src/schemas/metric-source.generated.ts create mode 100644 packages/shared/src/schemas/metric-source.schema.json create mode 100644 packages/shared/src/schemas/metric-source.schema.test.ts diff --git a/apps/dev-playground/config/queries/metric.json b/apps/dev-playground/config/queries/metric.json new file mode 100644 index 000000000..86fca1436 --- /dev/null +++ b/apps/dev-playground/config/queries/metric.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://databricks.github.io/appkit/schemas/metric-source.schema.json", + "sp": { + "revenue": { + "source": "appkit_demo.public.revenue_metrics" + } + }, + "obo": {} +} diff --git a/docs/static/schemas/metric-source.schema.json b/docs/static/schemas/metric-source.schema.json new file mode 100644 index 000000000..a41ef9679 --- /dev/null +++ b/docs/static/schemas/metric-source.schema.json @@ -0,0 +1,58 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://databricks.github.io/appkit/schemas/metric-source.schema.json", + "title": "AppKit Metric Source Configuration", + "description": "Schema for AppKit metric.json — declares Unity Catalog Metric View sources for the analytics plugin's metric-view path. Each entry under sp/obo binds a metric key to a UC metric view FQN. Object form (rather than bare string) at v1 enables future per-entry option growth without breaking changes.", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Reference to the JSON Schema for validation" + }, + "sp": { + "type": "object", + "description": "Metric views queried as the service principal. Cache scope is shared across all users.", + "additionalProperties": { + "$ref": "#/$defs/metricEntry" + }, + "propertyNames": { + "$ref": "#/$defs/metricKey" + } + }, + "obo": { + "type": "object", + "description": "Metric views queried as the requesting user (on-behalf-of). Cache scope is per-user.", + "additionalProperties": { + "$ref": "#/$defs/metricEntry" + }, + "propertyNames": { + "$ref": "#/$defs/metricKey" + } + } + }, + "additionalProperties": false, + "$defs": { + "metricKey": { + "type": "string", + "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$", + "description": "Metric key. Must be a valid identifier (letters, digits, underscores; cannot start with a digit). Becomes the route key in POST /api/analytics/metric/:key, the hook argument in useMetricView('', ...), and the MetricRegistry augmentation key." + }, + "metricEntry": { + "type": "object", + "description": "A single metric view source declaration. v1 only accepts the 'source' field; future per-entry options (cacheTtl, defaultFilter, allowlists) ship as additive properties.", + "required": ["source"], + "properties": { + "source": { + "type": "string", + "pattern": "^[a-zA-Z0-9_][a-zA-Z0-9_-]*\\.[a-zA-Z0-9_][a-zA-Z0-9_-]*\\.[a-zA-Z0-9_][a-zA-Z0-9_-]*$", + "description": "Three-part Unity Catalog FQN of the metric view: ..", + "examples": [ + "appkit_demo.public.revenue_metrics", + "main.analytics.customer_metrics" + ] + } + }, + "additionalProperties": false + } + } +} diff --git a/knip.json b/knip.json index b777d8c2a..bde14adc6 100644 --- a/knip.json +++ b/knip.json @@ -8,19 +8,15 @@ ], "workspaces": { "packages/appkit": {}, - "packages/appkit-ui": { - "ignoreDependencies": ["tailwindcss", "tw-animate-css"] - } + "packages/appkit-ui": {} }, "ignore": [ "**/*.generated.ts", "**/*.example.tsx", - "**/*.css", "packages/appkit/src/plugins/vector-search/**", "template/**", "tools/**", "docs/**" ], - "ignoreDependencies": ["json-schema-to-typescript"], "ignoreBinaries": ["tarball"] } diff --git a/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts new file mode 100644 index 000000000..8f968f567 --- /dev/null +++ b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts @@ -0,0 +1,177 @@ +import { act, renderHook, waitFor } from "@testing-library/react"; +import { afterEach, describe, expect, test, vi } from "vitest"; + +// Mock connectSSE — capture callbacks so we can simulate SSE events. +let capturedCallbacks: { + onMessage?: (msg: { data: string }) => void; + onError?: (err: Error) => void; + signal?: AbortSignal; +} = {}; + +const mockConnectSSE = vi.fn().mockImplementation((opts: any) => { + capturedCallbacks = { + onMessage: opts.onMessage, + onError: opts.onError, + signal: opts.signal, + }; + return new Promise((resolve) => { + setTimeout(resolve, 0); + }); +}); + +vi.mock("@/js", () => ({ + connectSSE: (...args: unknown[]) => mockConnectSSE(...args), +})); + +import { useMetricView } from "../use-metric-view"; + +describe("useMetricView", () => { + afterEach(() => { + capturedCallbacks = {}; + vi.clearAllMocks(); + }); + + test("initial state is loading=true with autoStart (default)", () => { + const { result } = renderHook(() => + useMetricView("revenue", { measures: ["arr"] }), + ); + + expect(result.current.data).toBeNull(); + // autoStart triggers connect synchronously inside useEffect, so + // loading flips to true before the test inspects state. + expect(result.current.loading).toBe(true); + expect(result.current.error).toBeNull(); + }); + + test("connects to /api/analytics/metric/ with the request payload", () => { + renderHook(() => useMetricView("revenue", { measures: ["arr"] })); + + expect(mockConnectSSE).toHaveBeenCalledWith( + expect.objectContaining({ + url: "/api/analytics/metric/revenue", + payload: JSON.stringify({ + measures: ["arr"], + format: "JSON", + }), + }), + ); + }); + + test("includes limit in the payload when provided", () => { + renderHook(() => + useMetricView("revenue", { measures: ["arr"], limit: 10 }), + ); + + const payload = JSON.parse( + (mockConnectSSE.mock.calls[0][0] as any).payload, + ); + expect(payload).toEqual({ + measures: ["arr"], + limit: 10, + format: "JSON", + }); + }); + + test("populates data on a result event", async () => { + const { result } = renderHook(() => + useMetricView("revenue", { measures: ["arr"] }), + ); + + act(() => { + capturedCallbacks.onMessage?.({ + data: JSON.stringify({ + type: "result", + data: [{ arr: 1234567 }], + }), + }); + }); + + await waitFor(() => { + expect(result.current.loading).toBe(false); + }); + expect(result.current.data).toEqual([{ arr: 1234567 }]); + expect(result.current.error).toBeNull(); + }); + + test("sets error on a server error event", async () => { + const { result } = renderHook(() => + useMetricView("revenue", { measures: ["arr"] }), + ); + + act(() => { + capturedCallbacks.onMessage?.({ + data: JSON.stringify({ + type: "error", + error: "Bad measures", + code: "VALIDATION_ERROR", + }), + }); + }); + + await waitFor(() => { + expect(result.current.loading).toBe(false); + }); + expect(result.current.error).toBe("Bad measures"); + expect(result.current.data).toBeNull(); + }); + + test("surfaces a network failure via onError", async () => { + const { result } = renderHook(() => + useMetricView("revenue", { measures: ["arr"] }), + ); + + act(() => { + capturedCallbacks.onError?.(new Error("Failed to fetch")); + }); + + await waitFor(() => { + expect(result.current.loading).toBe(false); + }); + expect(result.current.error).toMatch(/Network error/); + }); + + test("does NOT auto-start when autoStart=false", () => { + renderHook(() => + useMetricView("revenue", { measures: ["arr"] }, { autoStart: false }), + ); + expect(mockConnectSSE).not.toHaveBeenCalled(); + }); + + test("aborts the in-flight request on unmount", () => { + const { unmount } = renderHook(() => + useMetricView("revenue", { measures: ["arr"] }), + ); + + expect(capturedCallbacks.signal?.aborted).toBe(false); + unmount(); + expect(capturedCallbacks.signal?.aborted).toBe(true); + }); + + test("rejects an empty metric key", () => { + expect(() => + renderHook(() => useMetricView("", { measures: ["arr"] } as any)), + ).toThrowError(/non-empty string/); + }); + + test("rejects ARROW format with a clear error (out of v1 scope)", async () => { + const { result } = renderHook(() => + useMetricView("revenue", { measures: ["arr"] }, { + format: "ARROW", + } as any), + ); + + act(() => { + capturedCallbacks.onMessage?.({ + data: JSON.stringify({ + type: "arrow", + statement_id: "s-1", + }), + }); + }); + + await waitFor(() => { + expect(result.current.loading).toBe(false); + }); + expect(result.current.error).toMatch(/ARROW format is not supported/); + }); +}); diff --git a/packages/appkit-ui/src/react/hooks/index.ts b/packages/appkit-ui/src/react/hooks/index.ts index a425b0109..7fed35ca2 100644 --- a/packages/appkit-ui/src/react/hooks/index.ts +++ b/packages/appkit-ui/src/react/hooks/index.ts @@ -1,10 +1,15 @@ export type { AnalyticsFormat, + DimensionKey, InferResultByFormat, InferRowType, InferServingChunk, InferServingRequest, InferServingResponse, + MeasureKey, + MetricKey, + MetricRegistry, + MetricRow, PluginRegistry, QueryRegistry, ServingAlias, @@ -12,6 +17,9 @@ export type { TypedArrowTable, UseAnalyticsQueryOptions, UseAnalyticsQueryResult, + UseMetricViewArgs, + UseMetricViewOptions, + UseMetricViewResult, } from "./types"; export { useAnalyticsQuery } from "./use-analytics-query"; export { @@ -19,6 +27,7 @@ export { type UseChartDataResult, useChartData, } from "./use-chart-data"; +export { useMetricView } from "./use-metric-view"; export { usePluginClientConfig } from "./use-plugin-config"; export { type UseServingInvokeOptions, diff --git a/packages/appkit-ui/src/react/hooks/types.ts b/packages/appkit-ui/src/react/hooks/types.ts index 03e943e2a..086661e7a 100644 --- a/packages/appkit-ui/src/react/hooks/types.ts +++ b/packages/appkit-ui/src/react/hooks/types.ts @@ -140,6 +140,105 @@ export interface ServingClientConfig { aliases: string[]; } +// ============================================================================ +// Metric View Registry (Phase 1 — measures only) +// ============================================================================ + +/** + * Metric View Registry — populated via TypeScript module augmentation by the + * AppKit type-generator (parallel to {@link QueryRegistry}). + * + * Each registered metric key contributes an entry whose shape carries the + * FQN, lane, and the structured measure / dimension lists harvested from the + * build-time DESCRIBE TABLE EXTENDED ... AS JSON call. + * + * @example + * ```ts + * declare module "@databricks/appkit-ui/react" { + * interface MetricRegistry { + * revenue: { + * key: "revenue"; + * source: "appkit_demo.public.revenue_metrics"; + * lane: "sp"; + * measures: { arr: number; mrr: number }; + * dimensions: Record; + * measureKeys: "arr" | "mrr"; + * dimensionKeys: never; + * }; + * } + * } + * ``` + */ +// biome-ignore lint/suspicious/noEmptyInterface: intentionally empty — populated via module augmentation +export interface MetricRegistry {} + +/** Resolves to MetricRegistry keys if any are populated, otherwise string. */ +export type MetricKey = AugmentedRegistry extends never + ? string + : AugmentedRegistry; + +/** The union of declared measure names for a registered metric key. */ +export type MeasureKey = K extends AugmentedRegistry + ? MetricRegistry[K] extends { measureKeys: infer M } + ? M extends string + ? M + : string + : string + : string; + +/** The union of declared dimension names for a registered metric key. */ +export type DimensionKey = K extends AugmentedRegistry + ? MetricRegistry[K] extends { dimensionKeys: infer D } + ? D extends string + ? D + : never + : never + : never; + +/** The "measures" entry on a registered metric — a record of name → row type. */ +type MetricMeasureMap = K extends AugmentedRegistry + ? MetricRegistry[K] extends { measures: infer M } + ? M extends Record + ? M + : Record + : Record + : Record; + +/** The "dimensions" entry on a registered metric — a record of name → row type. */ +type MetricDimensionMap = K extends AugmentedRegistry + ? MetricRegistry[K] extends { dimensions: infer D } + ? D extends Record + ? D + : Record + : Record + : Record; + +/** Full result row type for a registered metric (measures + dimensions). */ +export type MetricRow = MetricMeasureMap & MetricDimensionMap; + +/** Phase 1 args: only measures are accepted. */ +export interface UseMetricViewArgs { + measures: ReadonlyArray>; + /** Optional row cap. */ + limit?: number; +} + +/** Phase 1 options: format passthrough + autoStart toggle. */ +export interface UseMetricViewOptions { + format?: F; + /** Whether to fire the request automatically on mount. Default: true. */ + autoStart?: boolean; + /** Maximum size of the serialized request body in bytes. Default: 100 KiB. */ + maxParametersSize?: number; +} + +/** Phase 1 result shape: { data, loading, error }. */ +export interface UseMetricViewResult { + data: TRow[] | null; + loading: boolean; + error: string | null; +} + // ============================================================================ // Serving Endpoint Registry // ============================================================================ diff --git a/packages/appkit-ui/src/react/hooks/use-metric-view.ts b/packages/appkit-ui/src/react/hooks/use-metric-view.ts new file mode 100644 index 000000000..ee5b15c98 --- /dev/null +++ b/packages/appkit-ui/src/react/hooks/use-metric-view.ts @@ -0,0 +1,164 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { connectSSE } from "@/js"; +import type { + AnalyticsFormat, + MetricKey, + MetricRow, + UseMetricViewArgs, + UseMetricViewOptions, + UseMetricViewResult, +} from "./types"; + +/** + * Subscribe to a metric-view query over SSE. + * + * Phase 1 surface — accepts `{ measures }` only. Phase 2/3 widen to + * `dimensions`, `filter`, `timeGrain`. The hook signature mirrors + * `useAnalyticsQuery`'s shape so that adopters muscle-memorize the call + * pattern across the two hooks. + * + * @example + * ```tsx + * const { data, loading, error } = useMetricView("revenue", { + * measures: ["arr"], + * }); + * ``` + */ +export function useMetricView< + K extends MetricKey = MetricKey, + F extends AnalyticsFormat = "JSON", +>( + metricKey: K, + args: UseMetricViewArgs, + options: UseMetricViewOptions = {} as UseMetricViewOptions, +): UseMetricViewResult> { + if (!metricKey || metricKey.trim().length === 0) { + throw new Error("useMetricView: 'metricKey' must be a non-empty string."); + } + + const format = options.format ?? "JSON"; + const autoStart = options.autoStart ?? true; + const maxParametersSize = options.maxParametersSize ?? 100 * 1024; + + const url = `/api/analytics/metric/${encodeURIComponent(metricKey)}`; + + type ResultType = MetricRow; + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const abortControllerRef = useRef(null); + + const payload = useMemo(() => { + try { + const body = { + measures: [...args.measures], + ...(typeof args.limit === "number" ? { limit: args.limit } : {}), + format, + }; + const serialized = JSON.stringify(body); + const sizeInBytes = new Blob([serialized]).size; + if (sizeInBytes > maxParametersSize) { + throw new Error( + "useMetricView: Request body size exceeds the maximum allowed size", + ); + } + return serialized; + } catch (err) { + console.error("useMetricView: Failed to serialize request body", err); + return null; + } + }, [args.measures, args.limit, format, maxParametersSize]); + + const start = useCallback(() => { + if (payload === null) { + setError("Failed to serialize metric request body"); + return; + } + + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + setLoading(true); + setError(null); + setData(null); + + const abortController = new AbortController(); + abortControllerRef.current = abortController; + + connectSSE({ + url, + payload, + signal: abortController.signal, + onMessage: async (message) => { + try { + const parsed = JSON.parse(message.data); + + if (parsed.type === "result") { + setLoading(false); + setData(parsed.data as ResultType[]); + return; + } + + if (parsed.type === "arrow") { + // Arrow path is wired by the analytics route but Phase 1 of + // metric views does not officially support ARROW (out-of-scope + // per the PRD). Surface the absence as a clear error so apps + // using a future ARROW path get a deterministic signal. + setLoading(false); + setError( + "useMetricView: ARROW format is not supported at v1. Use format: 'JSON'.", + ); + return; + } + + if (parsed.type === "error" || parsed.error || parsed.code) { + const errorMsg = + parsed.error || parsed.message || "Unable to execute metric"; + setLoading(false); + setError(errorMsg); + if (parsed.code) { + console.error( + `[useMetricView] Code: ${parsed.code}, Message: ${errorMsg}`, + ); + } + return; + } + } catch (err) { + console.warn("[useMetricView] Malformed message received", err); + } + }, + onError: (err) => { + if (abortController.signal.aborted) return; + setLoading(false); + + let userMessage = "Unable to load data, please try again"; + + if (err instanceof Error) { + if (err.name === "AbortError") { + userMessage = "Request timed out, please try again"; + } else if (err.message.includes("Failed to fetch")) { + userMessage = "Network error. Please check your connection."; + } + console.error("[useMetricView] Error", { + metricKey, + error: err.message, + stack: err.stack, + }); + } + setError(userMessage); + }, + }); + }, [metricKey, payload, url]); + + useEffect(() => { + if (autoStart) { + start(); + } + return () => { + abortControllerRef.current?.abort(); + }; + }, [start, autoStart]); + + return { data, loading, error }; +} diff --git a/packages/appkit/src/plugins/analytics/analytics.ts b/packages/appkit/src/plugins/analytics/analytics.ts index d591e32f0..95dd8d50a 100644 --- a/packages/appkit/src/plugins/analytics/analytics.ts +++ b/packages/appkit/src/plugins/analytics/analytics.ts @@ -8,16 +8,24 @@ import type { } from "shared"; import { SQLWarehouseConnector } from "../../connectors"; import { getWarehouseId, getWorkspaceClient } from "../../context"; +import { AppKitError } from "../../errors"; import { createLogger } from "../../logging/logger"; import { Plugin, toPlugin } from "../../plugin"; import type { PluginManifest } from "../../registry"; import { queryDefaults } from "./defaults"; import manifest from "./manifest.json"; +import { + buildMetricSql, + composeMetricCacheKey, + loadMetricRegistry, + validateMetricRequest, +} from "./metric"; import { QueryProcessor } from "./query"; import type { AnalyticsQueryResponse, IAnalyticsConfig, IAnalyticsQueryRequest, + MetricRegistration, } from "./types"; const logger = createLogger("analytics"); @@ -33,6 +41,13 @@ export class AnalyticsPlugin extends Plugin { private SQLClient: SQLWarehouseConnector; private queryProcessor: QueryProcessor; + /** + * Metric-view registry loaded from `config/queries/metric.json` at server + * startup. Keys are stable; values carry the FQN, lane, and known + * measure/dimension names. Empty when no `metric.json` is present. + */ + private metricRegistry: Record = {}; + constructor(config: IAnalyticsConfig) { super(config); this.config = config; @@ -44,6 +59,23 @@ export class AnalyticsPlugin extends Plugin { }); } + /** + * Eagerly load the metric registry. Failures are logged at warn level (not + * thrown) so a malformed `metric.json` does not take down the whole app — + * the route handler returns a clean 404 for unregistered keys regardless. + */ + async setup(): Promise { + try { + this.metricRegistry = await loadMetricRegistry(); + } catch (err) { + logger.warn( + "Failed to load metric registry: %s", + err instanceof Error ? err.message : String(err), + ); + this.metricRegistry = {}; + } + } + injectRoutes(router: IAppRouter) { // Arrow data downloads always run as service principal and bypass the // interceptor chain (execute/executeStream). The original query execution @@ -66,6 +98,15 @@ export class AnalyticsPlugin extends Plugin { await this._handleQueryRoute(req, res); }, }); + + this.route(router, { + name: "metric", + method: "post", + path: "/metric/:key", + handler: async (req: express.Request, res: express.Response) => { + await this._handleMetricRoute(req, res); + }, + }); } /** @@ -209,6 +250,129 @@ export class AnalyticsPlugin extends Plugin { ); } + /** + * Handle a metric-view query against `POST /api/analytics/metric/:key`. + * + * Phase 1 surface: + * - body validated by zod (rejects unknown measures when the registry + * has build-time metadata) + * - SQL constructed as `SELECT MEASURE() FROM [LIMIT n]` + * - response uses the same SSE envelope as the existing query route + * - reuses the interceptor chain via `executeStream()` (telemetry, + * timeout, retry, cache) + * + * OBO dispatch is implemented but only the SP lane has callers in Phase 1. + * Phase 4 finalizes OBO + cache key composition. + */ + async _handleMetricRoute( + req: express.Request, + res: express.Response, + ): Promise { + const { key } = req.params; + + logger.debug(req, "Executing metric: %s", key); + + const event = logger.event(req); + event?.setComponent("analytics", "executeMetric").setContext("analytics", { + metric_key: key, + plugin: this.name, + }); + + if (!key) { + res.status(400).json({ error: "metric key is required" }); + return; + } + + const registration = this.metricRegistry[key]; + if (!registration) { + res.status(404).json({ error: `Metric "${key}" not registered` }); + return; + } + + let request: ReturnType; + try { + request = validateMetricRequest(registration, req.body ?? {}); + } catch (err) { + if (err instanceof AppKitError) { + res.status(err.statusCode).json({ + error: err.message, + code: err.code, + }); + return; + } + // Validator only throws ValidationError, but be defensive. + res.status(400).json({ + error: err instanceof Error ? err.message : "Invalid request body", + }); + return; + } + + const format = request.format ?? "JSON"; + const isAsUser = registration.lane === "obo"; + const executor = isAsUser ? this.asUser(req) : this; + const executorKey = isAsUser ? this.resolveUserId(req) : "sp"; + + const queryParameters = + format === "ARROW" + ? { + formatParameters: { + disposition: "EXTERNAL_LINKS", + format: "ARROW_STREAM", + }, + type: "arrow", + } + : { type: "result" }; + + const cacheKey = composeMetricCacheKey({ + metricKey: key, + measures: request.measures, + format, + executorKey, + limit: request.limit, + }); + + const defaultConfig: PluginExecuteConfig = { + ...queryDefaults, + cache: { + ...queryDefaults.cache, + cacheKey, + }, + }; + + const streamExecutionSettings: StreamExecutionSettings = { + default: defaultConfig, + }; + + await executor.executeStream( + res, + async (signal) => { + const { statement } = buildMetricSql(registration, request); + const result = await executor.query( + statement, + undefined, + queryParameters.formatParameters, + signal, + ); + return { type: queryParameters.type, ...result }; + }, + streamExecutionSettings, + executorKey, + ); + } + + /** + * Test-only seam: populate the metric registry without going through + * `setup()` (which reads `config/queries/metric.json` from disk). Tests + * exercise the route handler directly with synthetic registrations. + * + * @internal + */ + _setMetricRegistryForTesting( + registry: Record, + ): void { + this.metricRegistry = registry; + } + /** * Execute a SQL query using the current execution context. * diff --git a/packages/appkit/src/plugins/analytics/index.ts b/packages/appkit/src/plugins/analytics/index.ts index 9ad02125e..78d793663 100644 --- a/packages/appkit/src/plugins/analytics/index.ts +++ b/packages/appkit/src/plugins/analytics/index.ts @@ -1,2 +1,3 @@ export * from "./analytics"; +export * from "./metric"; export * from "./types"; diff --git a/packages/appkit/src/plugins/analytics/metric.ts b/packages/appkit/src/plugins/analytics/metric.ts new file mode 100644 index 000000000..14a4a2226 --- /dev/null +++ b/packages/appkit/src/plugins/analytics/metric.ts @@ -0,0 +1,305 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { z } from "zod"; +import { ValidationError } from "../../errors"; +import { createLogger } from "../../logging/logger"; +import type { + IAnalyticsMetricRequest, + MetricLane, + MetricRegistration, +} from "./types"; + +const logger = createLogger("analytics:metric"); + +/** + * Default queries directory. Mirrors `AppManager.queriesDir` so dev mode and + * production share a single source of truth. + */ +const QUERIES_DIR = path.resolve(process.cwd(), "config/queries"); +const METRIC_CONFIG_FILE = "metric.json"; + +const METRIC_KEY_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/; +const FQN_PATTERN = + /^[a-zA-Z0-9_][a-zA-Z0-9_-]*\.[a-zA-Z0-9_][a-zA-Z0-9_-]*\.[a-zA-Z0-9_][a-zA-Z0-9_-]*$/; + +/** + * v1 entry shape — only `source` is allowed. Future per-entry options grow + * additively without breaking changes. + */ +const metricEntrySchema = z + .object({ + source: z.string().regex(FQN_PATTERN, { + message: + "metric.source must be a three-part UC FQN ..", + }), + }) + .strict(); + +const metricLaneSchema = z + .record( + z.string().regex(METRIC_KEY_PATTERN, { + message: + "metric key must match /^[a-zA-Z_][a-zA-Z0-9_]*$/ (letters, digits, underscores; cannot start with a digit)", + }), + metricEntrySchema, + ) + .optional(); + +/** Top-level shape of metric.json. */ +const metricConfigSchema = z + .object({ + $schema: z.string().optional(), + sp: metricLaneSchema, + obo: metricLaneSchema, + }) + .strict(); + +/** + * Read and validate `config/queries/metric.json`. + * + * Returns an empty registry when the file is absent — the metric-view path is + * additive; apps that never adopt metric views must not pay any cost. + * + * The optional `metadata` argument carries build-time-extracted measure / + * dimension names produced by the type-generator. When omitted, the registry + * still loads but `knownMeasures` is empty and the validator can only do + * structural checks. + */ +export async function loadMetricRegistry( + metadata?: Record, + queriesDir: string = QUERIES_DIR, +): Promise> { + const metricPath = path.join(queriesDir, METRIC_CONFIG_FILE); + + let raw: string; + try { + raw = await fs.readFile(metricPath, "utf8"); + } catch (err) { + if ((err as NodeJS.ErrnoException).code === "ENOENT") { + return {}; + } + throw err; + } + + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch (err) { + throw new Error( + `Failed to parse metric.json at ${metricPath}: ${(err as Error).message}`, + ); + } + + const result = metricConfigSchema.safeParse(parsed); + if (!result.success) { + const issues = result.error.issues + .map((i) => `${i.path.join(".")}: ${i.message}`) + .join("; "); + throw new Error(`Invalid metric.json at ${metricPath}: ${issues}`); + } + + const registry: Record = {}; + const lanes: Array<[MetricLane, Record]> = [ + ["sp", result.data.sp ?? {}], + ["obo", result.data.obo ?? {}], + ]; + + for (const [lane, laneMap] of lanes) { + for (const [key, entry] of Object.entries(laneMap)) { + if (key in registry) { + throw new Error( + `Duplicate metric key "${key}": cannot appear in both sp and obo lanes.`, + ); + } + const meta = metadata?.[key]; + registry[key] = { + key, + source: entry.source, + lane, + knownMeasures: meta?.measures ?? [], + knownDimensions: meta?.dimensions ?? [], + }; + } + } + + logger.debug( + "Loaded metric registry: %d entry(ies)", + Object.keys(registry).length, + ); + return registry; +} + +/** + * Build a zod schema for the request body of POST /api/analytics/metric/:key. + * + * The schema is dynamic per metric: when `knownMeasures` is non-empty the + * `measures` array is constrained to that set. When empty (no build-time + * metadata available) any non-empty string is accepted and validation defers + * to the warehouse. + * + * Phase 1 body shape: `{ measures, format?, limit? }`. Phase 2/3 widen this. + */ +export function makeMetricRequestSchema( + registration: MetricRegistration, +): z.ZodType { + const baseMeasureSchema = z + .string() + .min(1, { message: "measure name cannot be empty" }); + + // When the registry has build-time metadata, narrow the measure schema to + // the declared measure names. Use a refinement (rather than `z.enum`) so we + // can construct the schema dynamically at runtime. + const knownMeasures = registration.knownMeasures; + const measureItemSchema = + knownMeasures.length > 0 + ? baseMeasureSchema.refine( + (name: string) => knownMeasures.includes(name), + { + message: `measure must be one of: ${knownMeasures.join(", ")}`, + }, + ) + : baseMeasureSchema; + + return z + .object({ + measures: z + .array(measureItemSchema) + .min(1, { message: "measures must contain at least one entry" }), + format: z.enum(["JSON", "ARROW"]).optional(), + limit: z + .number() + .int({ message: "limit must be an integer" }) + .positive({ message: "limit must be positive" }) + .optional(), + }) + .strict() as z.ZodType; +} + +/** + * Validate the request body against the metric's schema. + * + * Returns the parsed body on success; throws {@link ValidationError} with the + * canonical 400 shape on failure. Throwing keeps the route handler simple — + * the AppKit error pipeline handles the response shape. + */ +export function validateMetricRequest( + registration: MetricRegistration, + body: unknown, +): IAnalyticsMetricRequest { + const schema = makeMetricRequestSchema(registration); + const result = schema.safeParse(body); + if (!result.success) { + const detail = result.error.issues + .map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`) + .join("; "); + throw new ValidationError(`Invalid metric request body: ${detail}`, { + context: { + metric: registration.key, + issues: result.error.issues, + }, + }); + } + return result.data; +} + +/** + * SQL identifier safety guard — the FQN ships in the SQL string (it cannot be + * parameterized) so we belt-and-suspender the regex check at construction time. + * + * The build-time loader already enforces FQN_PATTERN; this is a runtime fence + * for any future code path that constructs SQL outside of the registry. + */ +function assertSafeFqn(fqn: string): void { + if (!FQN_PATTERN.test(fqn)) { + throw new Error( + `Refusing to build SQL: "${fqn}" is not a valid three-part UC FQN.`, + ); + } +} + +/** + * Validate measure names before they are interpolated into MEASURE(). + * + * Measure names cannot be parameterized — they are SQL identifiers, not + * literals. We restrict to a conservative identifier shape and assert + * presence in the build-time registry when known. + */ +const MEASURE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/; + +/** + * Construct the Phase 1 metric SQL. + * + * Shape: `SELECT MEASURE(m1), MEASURE(m2) FROM [LIMIT n]`. + * + * Phase 1 has no dimensions, no filter, no GROUP BY, no time-grain. Each of + * those is a follow-on phase with its own dedicated test surface. The intent + * here is the integration spine, not a feature-rich generator. + */ +export function buildMetricSql( + registration: MetricRegistration, + request: IAnalyticsMetricRequest, +): { statement: string; parameters: never[] } { + assertSafeFqn(registration.source); + + if (request.measures.length === 0) { + throw new Error("buildMetricSql requires at least one measure."); + } + + for (const m of request.measures) { + if (!MEASURE_NAME_PATTERN.test(m)) { + throw new Error( + `Refusing to build SQL: measure "${m}" is not a valid identifier.`, + ); + } + if ( + registration.knownMeasures.length > 0 && + !registration.knownMeasures.includes(m) + ) { + throw new Error( + `Refusing to build SQL: unknown measure "${m}" for metric "${registration.key}".`, + ); + } + } + + // Deterministic order so cache keys collapse semantically equivalent calls. + // Sort-before-hash composition is finalized in Phase 4; sorting the SELECT + // list here is the same idea applied to the SQL itself. + const measureClauses = [...request.measures] + .sort() + .map((m) => `MEASURE(${m})`) + .join(", "); + + const limitClause = + typeof request.limit === "number" && request.limit > 0 + ? ` LIMIT ${Math.floor(request.limit)}` + : ""; + + const statement = `SELECT ${measureClauses} FROM ${registration.source}${limitClause}`; + return { statement, parameters: [] }; +} + +/** + * Compose the Phase 1 cache key. + * + * Reserved namespace `metric:` separates metric-view caches from query + * caches. Phase 4 finalizes sort-before-hash composition; Phase 1 only needs + * the namespace to be reserved + a stable per-key/per-args/per-executor key + * so the cache test surface works. + */ +export function composeMetricCacheKey(input: { + metricKey: string; + measures: string[]; + format: string; + executorKey: string; + limit?: number; +}): string[] { + const sortedMeasures = [...input.measures].sort(); + return [ + "metric", + input.metricKey, + input.format, + sortedMeasures.join(","), + typeof input.limit === "number" ? String(input.limit) : "_", + input.executorKey, + ]; +} diff --git a/packages/appkit/src/plugins/analytics/tests/analytics.test.ts b/packages/appkit/src/plugins/analytics/tests/analytics.test.ts index ce351021e..cf2eb97b2 100644 --- a/packages/appkit/src/plugins/analytics/tests/analytics.test.ts +++ b/packages/appkit/src/plugins/analytics/tests/analytics.test.ts @@ -79,18 +79,23 @@ describe("Analytics Plugin", () => { }); describe("injectRoutes", () => { - test("should register single POST route for queries", () => { + test("should register POST routes for queries and metrics", () => { const plugin = new AnalyticsPlugin(config); const { router } = createMockRouter(); plugin.injectRoutes(router); - // Only 1 POST route - asUser is determined by .obo.sql file convention - expect(router.post).toHaveBeenCalledTimes(1); + // 2 POST routes: /query/:query_key (asUser via .obo.sql convention) + // and /metric/:key (asUser via metric.json lane). + expect(router.post).toHaveBeenCalledTimes(2); expect(router.post).toHaveBeenCalledWith( "/query/:query_key", expect.any(Function), ); + expect(router.post).toHaveBeenCalledWith( + "/metric/:key", + expect.any(Function), + ); }); test("should register GET route for arrow results", () => { diff --git a/packages/appkit/src/plugins/analytics/tests/metric.test.ts b/packages/appkit/src/plugins/analytics/tests/metric.test.ts new file mode 100644 index 000000000..9dc670034 --- /dev/null +++ b/packages/appkit/src/plugins/analytics/tests/metric.test.ts @@ -0,0 +1,567 @@ +import { + createMockRequest, + createMockResponse, + createMockRouter, + mockServiceContext, + setupDatabricksEnv, +} from "@tools/test-helpers"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { ServiceContext } from "../../../context/service-context"; +import { AnalyticsPlugin } from "../analytics"; +import { + buildMetricSql, + composeMetricCacheKey, + loadMetricRegistry, + makeMetricRequestSchema, + validateMetricRequest, +} from "../metric"; +import type { IAnalyticsConfig, MetricRegistration } from "../types"; + +// Mirror the analytics test cache mock so the interceptor chain wiring is +// real but storage is in-memory and synchronous. +const { mockCacheStore, mockCacheInstance } = vi.hoisted(() => { + const store = new Map(); + + const generateKey = (parts: unknown[], userKey: string): string => { + const { createHash } = require("node:crypto"); + const allParts = [userKey, ...parts]; + const serialized = JSON.stringify(allParts); + return createHash("sha256").update(serialized).digest("hex"); + }; + + const instance = { + get: vi.fn(), + set: vi.fn(), + delete: vi.fn(), + getOrExecute: vi.fn( + async (key: unknown[], fn: () => Promise, userKey: string) => { + const cacheKey = generateKey(key, userKey); + if (store.has(cacheKey)) { + return store.get(cacheKey); + } + const result = await fn(); + store.set(cacheKey, result); + return result; + }, + ), + generateKey: vi.fn((parts: unknown[], userKey: string) => + generateKey(parts, userKey), + ), + }; + + return { mockCacheStore: store, mockCacheInstance: instance }; +}); + +vi.mock("../../../cache", () => ({ + CacheManager: { + getInstanceSync: vi.fn(() => mockCacheInstance), + }, +})); + +const REVENUE_REGISTRATION: MetricRegistration = { + key: "revenue", + source: "appkit_demo.public.revenue_metrics", + lane: "sp", + knownMeasures: ["arr", "mrr"], + knownDimensions: ["region", "segment"], +}; + +describe("metric — pure helpers", () => { + describe("makeMetricRequestSchema / validateMetricRequest", () => { + test("accepts a request with a known measure", () => { + const parsed = validateMetricRequest(REVENUE_REGISTRATION, { + measures: ["arr"], + }); + expect(parsed.measures).toEqual(["arr"]); + expect(parsed.format).toBeUndefined(); + }); + + test("accepts format=ARROW (handled, even if hook discourages it)", () => { + const parsed = validateMetricRequest(REVENUE_REGISTRATION, { + measures: ["arr"], + format: "ARROW", + }); + expect(parsed.format).toBe("ARROW"); + }); + + test("rejects an unknown measure with a clear error", () => { + expect(() => + validateMetricRequest(REVENUE_REGISTRATION, { + measures: ["bogus"], + }), + ).toThrowError(/measures\.0/); + }); + + test("rejects an empty measures array", () => { + expect(() => + validateMetricRequest(REVENUE_REGISTRATION, { + measures: [], + }), + ).toThrowError(/measures must contain at least one entry/); + }); + + test("rejects a non-positive limit", () => { + expect(() => + validateMetricRequest(REVENUE_REGISTRATION, { + measures: ["arr"], + limit: -1, + }), + ).toThrowError(/limit must be positive/); + }); + + test("rejects unknown top-level fields (strict)", () => { + expect(() => + validateMetricRequest(REVENUE_REGISTRATION, { + measures: ["arr"], + dimensions: ["region"], // not allowed at v1 + }), + ).toThrowError(); + }); + + test("falls open when knownMeasures is empty", () => { + const looseRegistration: MetricRegistration = { + ...REVENUE_REGISTRATION, + knownMeasures: [], + }; + const parsed = validateMetricRequest(looseRegistration, { + measures: ["anything"], + }); + expect(parsed.measures).toEqual(["anything"]); + }); + + test("schema construction is stable across calls", () => { + const a = makeMetricRequestSchema(REVENUE_REGISTRATION); + const b = makeMetricRequestSchema(REVENUE_REGISTRATION); + expect(a.safeParse({ measures: ["arr"] }).success).toBe(true); + expect(b.safeParse({ measures: ["arr"] }).success).toBe(true); + }); + }); + + describe("buildMetricSql", () => { + test("renders SELECT MEASURE() FROM ", () => { + const { statement, parameters } = buildMetricSql(REVENUE_REGISTRATION, { + measures: ["arr"], + }); + expect(statement).toBe( + "SELECT MEASURE(arr) FROM appkit_demo.public.revenue_metrics", + ); + expect(parameters).toEqual([]); + }); + + test("sorts measures lexicographically for deterministic SQL", () => { + const { statement } = buildMetricSql(REVENUE_REGISTRATION, { + measures: ["mrr", "arr"], + }); + expect(statement).toBe( + "SELECT MEASURE(arr), MEASURE(mrr) FROM appkit_demo.public.revenue_metrics", + ); + }); + + test("appends LIMIT clause when limit is provided", () => { + const { statement } = buildMetricSql(REVENUE_REGISTRATION, { + measures: ["arr"], + limit: 10, + }); + expect(statement).toBe( + "SELECT MEASURE(arr) FROM appkit_demo.public.revenue_metrics LIMIT 10", + ); + }); + + test("rejects unknown measures (defense in depth past the validator)", () => { + expect(() => + buildMetricSql(REVENUE_REGISTRATION, { + measures: ["bogus"], + }), + ).toThrowError(/unknown measure/i); + }); + + test("rejects measures that are not valid identifiers", () => { + const looseRegistration: MetricRegistration = { + ...REVENUE_REGISTRATION, + knownMeasures: [], + }; + expect(() => + buildMetricSql(looseRegistration, { + measures: ["arr; DROP TABLE foo --"], + }), + ).toThrowError(/not a valid identifier/); + }); + + test("rejects FQNs that are not three-part", () => { + const badRegistration: MetricRegistration = { + ...REVENUE_REGISTRATION, + source: "some.bad", + knownMeasures: ["arr"], + }; + expect(() => + buildMetricSql(badRegistration, { measures: ["arr"] }), + ).toThrowError(/three-part UC FQN/); + }); + + test("rejects empty measures", () => { + expect(() => + buildMetricSql(REVENUE_REGISTRATION, { measures: [] }), + ).toThrowError(/at least one measure/); + }); + }); + + describe("composeMetricCacheKey", () => { + test("reserves the metric: namespace prefix", () => { + const key = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + format: "JSON", + executorKey: "sp", + }); + expect(key[0]).toBe("metric"); + }); + + test("normalizes measure order for cache hits across equivalent calls", () => { + const a = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr", "mrr"], + format: "JSON", + executorKey: "sp", + }); + const b = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["mrr", "arr"], + format: "JSON", + executorKey: "sp", + }); + expect(a).toEqual(b); + }); + + test("differentiates SP vs OBO via executorKey", () => { + const sp = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + format: "JSON", + executorKey: "sp", + }); + const obo = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + format: "JSON", + executorKey: "user-1", + }); + expect(sp).not.toEqual(obo); + }); + + test("differentiates calls with different limit values", () => { + const a = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + format: "JSON", + executorKey: "sp", + limit: 10, + }); + const b = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + format: "JSON", + executorKey: "sp", + limit: 100, + }); + expect(a).not.toEqual(b); + }); + }); +}); + +describe("loadMetricRegistry", () => { + let tmpDir: string; + + beforeEach(async () => { + const fs = await import("node:fs/promises"); + const os = await import("node:os"); + const path = await import("node:path"); + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "appkit-metric-test-")); + }); + + afterEach(async () => { + const fs = await import("node:fs/promises"); + await fs.rm(tmpDir, { recursive: true, force: true }); + }); + + test("returns an empty object when metric.json is absent", async () => { + const registry = await loadMetricRegistry(undefined, tmpDir); + expect(registry).toEqual({}); + }); + + test("loads a basic metric.json with a single SP entry", async () => { + const fs = await import("node:fs/promises"); + const path = await import("node:path"); + await fs.writeFile( + path.join(tmpDir, "metric.json"), + JSON.stringify({ + sp: { revenue: { source: "demo.public.revenue" } }, + }), + ); + const registry = await loadMetricRegistry(undefined, tmpDir); + expect(registry.revenue).toEqual({ + key: "revenue", + source: "demo.public.revenue", + lane: "sp", + knownMeasures: [], + knownDimensions: [], + }); + }); + + test("merges build-time metadata into knownMeasures/knownDimensions", async () => { + const fs = await import("node:fs/promises"); + const path = await import("node:path"); + await fs.writeFile( + path.join(tmpDir, "metric.json"), + JSON.stringify({ + sp: { revenue: { source: "demo.public.revenue" } }, + }), + ); + const registry = await loadMetricRegistry( + { revenue: { measures: ["arr"], dimensions: ["region"] } }, + tmpDir, + ); + expect(registry.revenue.knownMeasures).toEqual(["arr"]); + expect(registry.revenue.knownDimensions).toEqual(["region"]); + }); + + test("rejects unknown fields on entries (strict)", async () => { + const fs = await import("node:fs/promises"); + const path = await import("node:path"); + await fs.writeFile( + path.join(tmpDir, "metric.json"), + JSON.stringify({ + sp: { + revenue: { + source: "demo.public.revenue", + cacheTtl: 60, // not allowed at v1 + }, + }, + }), + ); + await expect(loadMetricRegistry(undefined, tmpDir)).rejects.toThrowError( + /Invalid metric.json/, + ); + }); + + test("rejects bad FQN format", async () => { + const fs = await import("node:fs/promises"); + const path = await import("node:path"); + await fs.writeFile( + path.join(tmpDir, "metric.json"), + JSON.stringify({ + sp: { revenue: { source: "not.a.three.part" } }, + }), + ); + await expect(loadMetricRegistry(undefined, tmpDir)).rejects.toThrowError( + /three-part UC FQN/, + ); + }); + + test("rejects duplicate keys across sp/obo lanes", async () => { + const fs = await import("node:fs/promises"); + const path = await import("node:path"); + await fs.writeFile( + path.join(tmpDir, "metric.json"), + JSON.stringify({ + sp: { revenue: { source: "demo.public.revenue" } }, + obo: { revenue: { source: "demo.public.revenue" } }, + }), + ); + await expect(loadMetricRegistry(undefined, tmpDir)).rejects.toThrowError( + /Duplicate metric key/, + ); + }); + + test("rejects an obo entry with the same key as sp", async () => { + const fs = await import("node:fs/promises"); + const path = await import("node:path"); + await fs.writeFile( + path.join(tmpDir, "metric.json"), + JSON.stringify({ + sp: { foo: { source: "demo.public.foo" } }, + obo: { foo: { source: "demo.public.foo" } }, + }), + ); + await expect(loadMetricRegistry(undefined, tmpDir)).rejects.toThrow(); + }); + + test("rejects malformed JSON", async () => { + const fs = await import("node:fs/promises"); + const path = await import("node:path"); + await fs.writeFile(path.join(tmpDir, "metric.json"), "{not json"); + await expect(loadMetricRegistry(undefined, tmpDir)).rejects.toThrowError( + /parse metric.json/, + ); + }); + + test("rejects metric keys that start with a digit", async () => { + const fs = await import("node:fs/promises"); + const path = await import("node:path"); + await fs.writeFile( + path.join(tmpDir, "metric.json"), + JSON.stringify({ + sp: { "1revenue": { source: "demo.public.revenue" } }, + }), + ); + await expect(loadMetricRegistry(undefined, tmpDir)).rejects.toThrow(); + }); +}); + +describe("AnalyticsPlugin — metric route handler", () => { + let config: IAnalyticsConfig; + let serviceContextMock: Awaited>; + + beforeEach(async () => { + config = { timeout: 5000 }; + setupDatabricksEnv(); + mockCacheStore.clear(); + ServiceContext.reset(); + serviceContextMock = await mockServiceContext(); + }); + + afterEach(() => { + serviceContextMock?.restore(); + }); + + test("returns 404 for an unregistered metric key", async () => { + const plugin = new AnalyticsPlugin(config); + const { router, getHandler } = createMockRouter(); + + plugin.injectRoutes(router); + + const handler = getHandler("POST", "/metric/:key"); + const mockReq = createMockRequest({ + params: { key: "ghost" }, + body: { measures: ["arr"] }, + }); + const mockRes = createMockResponse(); + + await handler(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(404); + expect(mockRes.json).toHaveBeenCalledWith({ + error: 'Metric "ghost" not registered', + }); + }); + + test("returns 400 with the canonical error shape on validator failure", async () => { + const plugin = new AnalyticsPlugin(config); + plugin._setMetricRegistryForTesting({ + revenue: REVENUE_REGISTRATION, + }); + const { router, getHandler } = createMockRouter(); + + plugin.injectRoutes(router); + + const handler = getHandler("POST", "/metric/:key"); + const mockReq = createMockRequest({ + params: { key: "revenue" }, + body: { measures: ["bogus"] }, + }); + const mockRes = createMockResponse(); + + await handler(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(400); + const errorPayload = (mockRes.json as any).mock.calls[0][0]; + expect(errorPayload.error).toMatch(/Invalid metric request body/); + expect(errorPayload.code).toBe("VALIDATION_ERROR"); + }); + + test("returns 400 when measures array is missing", async () => { + const plugin = new AnalyticsPlugin(config); + plugin._setMetricRegistryForTesting({ + revenue: REVENUE_REGISTRATION, + }); + const { router, getHandler } = createMockRouter(); + + plugin.injectRoutes(router); + + const handler = getHandler("POST", "/metric/:key"); + const mockReq = createMockRequest({ + params: { key: "revenue" }, + body: {}, + }); + const mockRes = createMockResponse(); + + await handler(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(400); + }); + + test("executes a valid SP metric request and streams a result event", async () => { + const plugin = new AnalyticsPlugin(config); + plugin._setMetricRegistryForTesting({ + revenue: REVENUE_REGISTRATION, + }); + const { router, getHandler } = createMockRouter(); + + const executeMock = vi.fn().mockResolvedValue({ + result: { data: [{ arr: 1234567 }] }, + }); + (plugin as any).SQLClient.executeStatement = executeMock; + + plugin.injectRoutes(router); + + const handler = getHandler("POST", "/metric/:key"); + const mockReq = createMockRequest({ + params: { key: "revenue" }, + body: { measures: ["arr"] }, + }); + const mockRes = createMockResponse(); + + await handler(mockReq, mockRes); + + // Verify the constructed SQL hit the warehouse connector. + expect(executeMock).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + statement: + "SELECT MEASURE(arr) FROM appkit_demo.public.revenue_metrics", + warehouse_id: "test-warehouse-id", + }), + expect.any(AbortSignal), + ); + + expect(mockRes.setHeader).toHaveBeenCalledWith( + "Content-Type", + "text/event-stream", + ); + expect(mockRes.write).toHaveBeenCalledWith("event: result\n"); + expect(mockRes.write).toHaveBeenCalledWith( + expect.stringContaining('"arr":1234567'), + ); + expect(mockRes.end).toHaveBeenCalled(); + }); + + test("hits the cache on the second identical SP request", async () => { + const plugin = new AnalyticsPlugin(config); + plugin._setMetricRegistryForTesting({ + revenue: REVENUE_REGISTRATION, + }); + const { router, getHandler } = createMockRouter(); + + const executeMock = vi.fn().mockResolvedValue({ + result: { data: [{ arr: 1234567 }] }, + }); + (plugin as any).SQLClient.executeStatement = executeMock; + + plugin.injectRoutes(router); + + const handler = getHandler("POST", "/metric/:key"); + + const mockReq1 = createMockRequest({ + params: { key: "revenue" }, + body: { measures: ["arr"] }, + }); + const mockReq2 = createMockRequest({ + params: { key: "revenue" }, + body: { measures: ["arr"] }, + }); + + await handler(mockReq1, createMockResponse()); + await handler(mockReq2, createMockResponse()); + + expect(executeMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/appkit/src/plugins/analytics/types.ts b/packages/appkit/src/plugins/analytics/types.ts index c58b6ecfe..00a55a1dd 100644 --- a/packages/appkit/src/plugins/analytics/types.ts +++ b/packages/appkit/src/plugins/analytics/types.ts @@ -16,3 +16,49 @@ export interface AnalyticsQueryResponse { row_count: number; data: any[]; } + +/** + * Lane an entry sits in inside metric.json. SP = service principal (shared + * cache), OBO = on-behalf-of (per-user cache). + */ +export type MetricLane = "sp" | "obo"; + +/** + * Resolved metric-view registration loaded at server startup. + * + * The registration carries the FQN (used by the SQL constructor) plus the + * known measure/dimension names produced by the build-time DESCRIBE call + * (used by the body validator to reject unknown measures fast). + */ +export interface MetricRegistration { + /** Stable map key from metric.json. */ + key: string; + /** Three-part Unity Catalog FQN of the metric view. */ + source: string; + /** Lane this metric was registered under. */ + lane: MetricLane; + /** + * Names of measures known at build time. Empty array means "unknown" — at + * Phase 1 the server falls open in that case (relies on the warehouse to + * reject bad column references). Phase 2 tightens this. + */ + knownMeasures: string[]; + /** + * Names of dimensions known at build time. Phase 1 does not consume these + * (no GROUP BY yet) but they ride along so Phase 2/3 do not need a second + * loader. + */ + knownDimensions: string[]; +} + +/** + * Body of POST /api/analytics/metric/:key at Phase 1. + * + * Phase 2 widens this with `dimensions` + `timeGrain`; Phase 3 adds `filter`. + */ +export interface IAnalyticsMetricRequest { + measures: string[]; + format?: AnalyticsFormat; + /** Optional row cap. Phase 1 only. */ + limit?: number; +} diff --git a/packages/appkit/src/type-generator/index.ts b/packages/appkit/src/type-generator/index.ts index c9a528fe7..0b700dc10 100644 --- a/packages/appkit/src/type-generator/index.ts +++ b/packages/appkit/src/type-generator/index.ts @@ -2,6 +2,15 @@ import fs from "node:fs/promises"; import path from "node:path"; import dotenv from "dotenv"; import { createLogger } from "../logging/logger"; +import { + createWorkspaceDescribeFetcher, + type DescribeFetcher, + generateMetricTypeDeclarations, + type MetricSchema, + readMetricConfig, + resolveMetricConfig, + syncMetrics, +} from "./metric-registry"; import { migrateProjectConfig, removeOldGeneratedTypes, @@ -50,15 +59,29 @@ declare module "@databricks/appkit-ui/react" { * @param options - the options for the generation * @param options.entryPoint - the entry point file * @param options.outFile - the output file - * @param options.querySchemaFile - optional path to query schema file (e.g. config/queries/schema.ts) + * @param options.metricOutFile - optional output file for the MetricRegistry + * augmentation. Defaults to a sibling `metric.d.ts` file under the same + * directory as `outFile`. Skipped entirely if `metric.json` is absent. + * @param options.metricFetcher - optional DescribeFetcher used by + * {@link syncMetrics}. Tests inject a mock; production builds let the + * default WorkspaceClient-backed fetcher be created lazily. */ export async function generateFromEntryPoint(options: { outFile: string; queryFolder?: string; warehouseId: string; noCache?: boolean; + metricOutFile?: string; + metricFetcher?: DescribeFetcher; }) { - const { outFile, queryFolder, warehouseId, noCache } = options; + const { + outFile, + queryFolder, + warehouseId, + noCache, + metricOutFile, + metricFetcher, + } = options; const projectRoot = resolveProjectRoot(outFile); logger.debug("Starting type generation..."); @@ -93,6 +116,31 @@ export async function generateFromEntryPoint(options: { await fs.mkdir(path.dirname(outFile), { recursive: true }); await fs.writeFile(outFile, typeDeclarations, "utf-8"); + // Metric-view types: only emit when metric.json exists. The path is purely + // additive — apps that never adopt metric views must not produce empty noise. + if (queryFolder) { + const metricConfig = await readMetricConfig(queryFolder); + if (metricConfig) { + const resolution = resolveMetricConfig(metricConfig); + const fetcher = + metricFetcher ?? createWorkspaceDescribeFetcher(warehouseId); + const metricSchemas: MetricSchema[] = await syncMetrics( + resolution, + fetcher, + ); + + const metricFile = + metricOutFile ?? path.join(path.dirname(outFile), METRIC_TYPES_FILE); + const metricDeclarations = generateMetricTypeDeclarations(metricSchemas); + await fs.mkdir(path.dirname(metricFile), { recursive: true }); + await fs.writeFile(metricFile, metricDeclarations, "utf-8"); + logger.debug( + "Wrote MetricRegistry augmentation for %d metric(s)", + metricSchemas.length, + ); + } + } + // One-time migration: remove old generated file and patch project configs await removeOldGeneratedTypes(projectRoot, "appKitTypes.d.ts"); await migrateProjectConfig(projectRoot); @@ -111,3 +159,5 @@ export const TYPES_DIR = "appkit-types"; export const ANALYTICS_TYPES_FILE = "analytics.d.ts"; /** Default filename for serving endpoint type declarations. */ export const SERVING_TYPES_FILE = "serving.d.ts"; +/** Default filename for metric-view registry type declarations. */ +export const METRIC_TYPES_FILE = "metric.d.ts"; diff --git a/packages/appkit/src/type-generator/metric-registry.ts b/packages/appkit/src/type-generator/metric-registry.ts new file mode 100644 index 000000000..b3f4d2fc2 --- /dev/null +++ b/packages/appkit/src/type-generator/metric-registry.ts @@ -0,0 +1,545 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { WorkspaceClient } from "@databricks/sdk-experimental"; +import { createLogger } from "../logging/logger"; +import type { DatabricksStatementExecutionResponse } from "./types"; + +const logger = createLogger("type-generator:metric-registry"); + +/** + * Default filename for the metric source declarations. + * Lives at config/queries/metric.json by convention. + */ +const METRIC_CONFIG_FILE = "metric.json"; + +/** + * The lane an entry sits in: `sp` (service principal, shared cache) + * or `obo` (on-behalf-of, per-user cache). + */ +export type MetricLane = "sp" | "obo"; + +/** + * Single entry in metric.json. + * + * v1 only allows `source`. Object form (rather than bare string) is the + * forward-compat seam for future per-entry options (cacheTtl, defaultFilter, ...). + */ +interface MetricEntryConfig { + source: string; +} + +/** + * Shape of metric.json (matches MetricSourceConfiguration generated from the JSON Schema). + * Inlined here so the type-generator does not pull in the shared schema package at runtime. + */ +interface MetricSourceConfig { + $schema?: string; + sp?: Record; + obo?: Record; +} + +/** + * Resolved entry consumed by the rest of the metric-view pipeline. + * Lane is denormalized onto the entry so downstream code does not have to + * track which top-level key it came from. + */ +interface ResolvedMetricEntry { + /** Stable map key shared across route, hook, registry, and cache. */ + key: string; + /** Three-part Unity Catalog FQN of the metric view. */ + source: string; + /** Execution lane — sp = service principal, obo = on-behalf-of. */ + lane: MetricLane; +} + +/** + * Per-column metadata extracted from DESCRIBE TABLE EXTENDED ... AS JSON. + * + * We only need a small subset at Phase 1 (measure names + types). Dimensions + * and YAML metadata land in later phases. + */ +export interface MetricColumnMetadata { + name: string; + type: string; + /** UC marks columns produced by `MEASURE()` as measures; everything else is a dimension. */ + isMeasure: boolean; + /** Optional column comment / display description (best-effort). */ + description?: string; +} + +/** + * Per-metric schema captured at type-generation time. + * + * The full row type is the union of measure + dimension column types. Phase 1 + * uses only `measures`; Phase 2 widens to `dimensions` and `timeGrains`. + */ +export interface MetricSchema { + /** Stable metric key (the map key in metric.json). */ + key: string; + /** Three-part FQN of the metric view. */ + source: string; + /** Execution lane this metric was registered under. */ + lane: MetricLane; + /** Measure columns (those exposed by MEASURE()). */ + measures: MetricColumnMetadata[]; + /** Dimension columns (everything that is not a measure). */ + dimensions: MetricColumnMetadata[]; +} + +/** + * Result of reading and resolving metric.json — split by lane plus a flat + * list with lane denormalized for iteration. + */ +interface MetricConfigResolution { + entries: ResolvedMetricEntry[]; +} + +/** + * Read metric.json from a queries folder. + * + * Returns `null` if the file does not exist (the metric-view path is + * additive — apps without metric.json must not be penalized). + * + * Throws on JSON parse errors so misconfiguration surfaces loudly. + */ +export async function readMetricConfig( + queryFolder: string, +): Promise { + const metricPath = path.join(queryFolder, METRIC_CONFIG_FILE); + let raw: string; + try { + raw = await fs.readFile(metricPath, "utf8"); + } catch (err) { + if ((err as NodeJS.ErrnoException).code === "ENOENT") { + return null; + } + throw err; + } + + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch (err) { + throw new Error( + `Failed to parse metric.json at ${metricPath}: ${(err as Error).message}`, + ); + } + + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + throw new Error( + `Invalid metric.json at ${metricPath}: expected an object with sp/obo keys.`, + ); + } + + return parsed as MetricSourceConfig; +} + +/** + * Validate a key against the JSON Schema's metricKey pattern. Phase 1 keeps + * this lightweight — the JSON Schema is the canonical contract for IDE/CI. + */ +function isValidMetricKey(key: string): boolean { + return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key); +} + +/** + * Validate a UC FQN against the JSON Schema's source pattern. + */ +function isValidFqn(fqn: string): boolean { + return /^[a-zA-Z0-9_][a-zA-Z0-9_-]*\.[a-zA-Z0-9_][a-zA-Z0-9_-]*\.[a-zA-Z0-9_][a-zA-Z0-9_-]*$/.test( + fqn, + ); +} + +/** + * Flatten the sp/obo map into a single list of resolved entries. + * + * Throws on duplicate keys across lanes (the same key cannot live in both), + * invalid keys, or invalid FQNs. Stable ordering: sp lane first, alphabetical. + */ +export function resolveMetricConfig( + config: MetricSourceConfig, +): MetricConfigResolution { + const entries: ResolvedMetricEntry[] = []; + const seen = new Set(); + + const lanes: Array<[MetricLane, Record]> = [ + ["sp", config.sp ?? {}], + ["obo", config.obo ?? {}], + ]; + + for (const [lane, laneMap] of lanes) { + const sortedKeys = Object.keys(laneMap).sort(); + for (const key of sortedKeys) { + if (!isValidMetricKey(key)) { + throw new Error( + `Invalid metric key "${key}" in lane "${lane}": must match /^[a-zA-Z_][a-zA-Z0-9_]*$/.`, + ); + } + + if (seen.has(key)) { + throw new Error( + `Duplicate metric key "${key}": cannot appear in both sp and obo lanes.`, + ); + } + seen.add(key); + + const entry = laneMap[key]; + if (!entry || typeof entry !== "object" || Array.isArray(entry)) { + throw new Error( + `Invalid metric entry "${key}" in lane "${lane}": expected an object with a 'source' field.`, + ); + } + + // v1 explicitly rejects unknown fields so future additions cannot be + // silently consumed today. + const allowed = new Set(["source"]); + for (const field of Object.keys(entry)) { + if (!allowed.has(field)) { + throw new Error( + `Invalid field "${field}" on metric entry "${key}": only 'source' is allowed at v1.`, + ); + } + } + + if (typeof entry.source !== "string" || entry.source.trim() === "") { + throw new Error( + `Invalid metric entry "${key}" in lane "${lane}": 'source' must be a non-empty string.`, + ); + } + + if (!isValidFqn(entry.source)) { + throw new Error( + `Invalid metric source "${entry.source}" for "${key}": expected a three-part UC FQN ...`, + ); + } + + entries.push({ key, source: entry.source, lane }); + } + } + + return { entries }; +} + +/** + * Parse the JSON payload returned by DESCRIBE TABLE EXTENDED ... AS JSON. + * + * The Statement Execution API returns a single string cell — this normalizer + * unwraps it. Handles both the production (real warehouse) shape and the + * shape produced by mocked test responses. + */ +export function parseDescribeTableExtendedJson( + response: DatabricksStatementExecutionResponse, +): unknown { + if (response.status?.state === "FAILED") { + const msg = response.status.error?.message ?? "DESCRIBE failed"; + throw new Error(`DESCRIBE TABLE EXTENDED failed: ${msg}`); + } + + const rows = response.result?.data_array ?? []; + if (rows.length === 0) { + throw new Error( + "DESCRIBE TABLE EXTENDED returned no rows. Verify the FQN points to a metric view.", + ); + } + + const cell = rows[0]?.[0]; + if (typeof cell !== "string") { + throw new Error( + "DESCRIBE TABLE EXTENDED first cell was not a JSON string. Confirm the AS JSON suffix is supported.", + ); + } + + try { + return JSON.parse(cell); + } catch (err) { + throw new Error( + `Failed to parse DESCRIBE TABLE EXTENDED JSON: ${(err as Error).message}`, + ); + } +} + +/** + * Pure function: turn the parsed DESCRIBE JSON into structured column metadata. + * + * Tolerant of multiple JSON shapes (the field may be `columns` or `schema.fields`, + * type may be a string or `{ name }` object, the measure marker may be `is_measure` + * or under `metadata.is_measure`). Phase 1's job is to find names + measure flags; + * later phases can tighten this if a more authoritative shape stabilizes. + */ +export function extractMetricColumns(parsed: unknown): MetricColumnMetadata[] { + if (!parsed || typeof parsed !== "object") { + return []; + } + + const root = parsed as Record; + const columnsCandidate = (root.columns ?? + (root.schema && typeof root.schema === "object" + ? (root.schema as Record).fields + : undefined)) as unknown; + + if (!Array.isArray(columnsCandidate)) { + return []; + } + + const columns: MetricColumnMetadata[] = []; + for (const raw of columnsCandidate) { + if (!raw || typeof raw !== "object") continue; + const obj = raw as Record; + const name = + typeof obj.name === "string" + ? obj.name + : typeof obj.column_name === "string" + ? obj.column_name + : undefined; + if (!name) continue; + + const typeRaw = obj.type ?? obj.data_type ?? obj.type_name; + let type = "STRING"; + if (typeof typeRaw === "string") { + type = typeRaw; + } else if (typeRaw && typeof typeRaw === "object") { + const inner = (typeRaw as Record).name; + if (typeof inner === "string") type = inner; + } + + let isMeasure = false; + if (typeof obj.is_measure === "boolean") { + isMeasure = obj.is_measure; + } else if ( + obj.metadata && + typeof obj.metadata === "object" && + typeof (obj.metadata as Record).is_measure === "boolean" + ) { + isMeasure = (obj.metadata as Record) + .is_measure as boolean; + } else if (obj.kind === "measure" || obj.role === "measure") { + isMeasure = true; + } + + const description = + typeof obj.comment === "string" + ? obj.comment + : typeof obj.description === "string" + ? obj.description + : undefined; + + columns.push({ name, type, isMeasure, description }); + } + + return columns; +} + +/** + * Map a Databricks SQL type to a TypeScript primitive. + * Centralized here (not imported from query-registry) so this module + * stays self-contained at Phase 1. + */ +function tsTypeFor(sqlType: string): string { + const normalized = sqlType + .toUpperCase() + .replace(/\(.*\)$/, "") + .replace(/<.*>$/, "") + .split(" ")[0]; + + switch (normalized) { + case "BOOLEAN": + return "boolean"; + case "TINYINT": + case "SMALLINT": + case "INT": + case "INTEGER": + case "BIGINT": + case "FLOAT": + case "DOUBLE": + case "DECIMAL": + case "NUMERIC": + return "number"; + default: + return "string"; + } +} + +/** + * Render a MetricRegistry interface entry from a MetricSchema. + */ +function renderMetricEntry(schema: MetricSchema): string { + const indent = " "; + const measures = + schema.measures.length > 0 + ? schema.measures + .map( + (m) => `${indent}/** @sqlType ${m.type} */ +${indent}${JSON.stringify(m.name)}: ${tsTypeFor(m.type)}`, + ) + .join(";\n") + : ""; + const dimensions = + schema.dimensions.length > 0 + ? schema.dimensions + .map( + (d) => `${indent}/** @sqlType ${d.type} */ +${indent}${JSON.stringify(d.name)}: ${tsTypeFor(d.type)}`, + ) + .join(";\n") + : ""; + + const measureKeys = schema.measures.map((m) => JSON.stringify(m.name)); + const dimensionKeys = schema.dimensions.map((d) => JSON.stringify(d.name)); + + const measuresBlock = measures + ? `{ +${measures}; + }` + : "Record"; + + const dimensionsBlock = dimensions + ? `{ +${dimensions}; + }` + : "Record"; + + const measureUnion = + measureKeys.length > 0 ? measureKeys.join(" | ") : "never"; + const dimensionUnion = + dimensionKeys.length > 0 ? dimensionKeys.join(" | ") : "never"; + + return ` ${JSON.stringify(schema.key)}: { + key: ${JSON.stringify(schema.key)}; + source: ${JSON.stringify(schema.source)}; + lane: ${JSON.stringify(schema.lane)}; + measures: ${measuresBlock}; + dimensions: ${dimensionsBlock}; + measureKeys: ${measureUnion}; + dimensionKeys: ${dimensionUnion}; + }`; +} + +/** + * Render the augmentation block for the appkit-ui MetricRegistry interface. + * + * Mirrors the pattern in `generateTypeDeclarations` for QueryRegistry — emits + * a `declare module` block that consumers in `@databricks/appkit-ui/react` + * pick up via TypeScript module augmentation. + */ +function renderMetricRegistry(schemas: MetricSchema[]): string { + if (schemas.length === 0) { + return `declare module "@databricks/appkit-ui/react" { + interface MetricRegistry {} +} +`; + } + const entries = schemas.map(renderMetricEntry).join(";\n"); + return `declare module "@databricks/appkit-ui/react" { + interface MetricRegistry { +${entries}; + } +} +`; +} + +/** + * Default header for the generated metric.d.ts file. The file is consumed by + * TypeScript via module augmentation only, so no runtime import is needed. + */ +function metricFileHeader(): string { + return `// Auto-generated by AppKit - DO NOT EDIT +// Generated by 'npx @databricks/appkit generate-types' or Vite plugin during build +import "@databricks/appkit-ui/react"; +`; +} + +/** + * Build the full metric.d.ts file from a list of metric schemas. + */ +export function generateMetricTypeDeclarations( + schemas: MetricSchema[], +): string { + return metricFileHeader() + renderMetricRegistry(schemas); +} + +/** + * Optional dependency-injection seam: the function used to fetch DESCRIBE + * results for a given FQN. Production wires this through the WorkspaceClient; + * tests inject a mock that returns a representative DESCRIBE response. + */ +export type DescribeFetcher = ( + fqn: string, +) => Promise; + +/** + * Build a DescribeFetcher from a real WorkspaceClient + warehouseId. + * + * Kept narrow so it does not require importing the SDK at test time. + */ +export function createWorkspaceDescribeFetcher( + warehouseId: string, +): DescribeFetcher { + const client = new WorkspaceClient({}); + return async (fqn: string) => { + const result = (await client.statementExecution.executeStatement({ + statement: `DESCRIBE TABLE EXTENDED ${fqn} AS JSON`, + warehouse_id: warehouseId, + })) as DatabricksStatementExecutionResponse; + return result; + }; +} + +/** + * Run schema synchronization for every entry in `metric.json`. + * + * `fetcher` is injected so the same code path serves Vite, the (Phase 6) CLI, + * and unit tests with a mock that returns a representative DESCRIBE response. + */ +export async function syncMetrics( + resolution: MetricConfigResolution, + fetcher: DescribeFetcher, +): Promise { + const schemas: MetricSchema[] = []; + + for (const entry of resolution.entries) { + let response: DatabricksStatementExecutionResponse; + try { + response = await fetcher(entry.source); + } catch (err) { + logger.warn( + "DESCRIBE TABLE EXTENDED failed for %s: %s", + entry.source, + (err as Error).message, + ); + schemas.push({ + key: entry.key, + source: entry.source, + lane: entry.lane, + measures: [], + dimensions: [], + }); + continue; + } + + let columns: MetricColumnMetadata[] = []; + try { + const parsed = parseDescribeTableExtendedJson(response); + columns = extractMetricColumns(parsed); + } catch (err) { + logger.warn( + "Failed to extract columns from DESCRIBE response for %s: %s", + entry.source, + (err as Error).message, + ); + } + + const measures = columns.filter((c) => c.isMeasure); + const dimensions = columns.filter((c) => !c.isMeasure); + + schemas.push({ + key: entry.key, + source: entry.source, + lane: entry.lane, + measures, + dimensions, + }); + } + + return schemas; +} diff --git a/packages/appkit/src/type-generator/tests/__snapshots__/metric-registry.test.ts.snap b/packages/appkit/src/type-generator/tests/__snapshots__/metric-registry.test.ts.snap new file mode 100644 index 000000000..ae98e096f --- /dev/null +++ b/packages/appkit/src/type-generator/tests/__snapshots__/metric-registry.test.ts.snap @@ -0,0 +1,41 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`generateMetricTypeDeclarations — snapshot > emits a stable MetricRegistry augmentation for a representative input 1`] = ` +"// Auto-generated by AppKit - DO NOT EDIT +// Generated by 'npx @databricks/appkit generate-types' or Vite plugin during build +import "@databricks/appkit-ui/react"; +declare module "@databricks/appkit-ui/react" { + interface MetricRegistry { + "revenue": { + key: "revenue"; + source: "appkit_demo.public.revenue_metrics"; + lane: "sp"; + measures: { + /** @sqlType DECIMAL(38,2) */ + "arr": number; + /** @sqlType DECIMAL(38,2) */ + "mrr": number; + }; + dimensions: { + /** @sqlType STRING */ + "region": string; + /** @sqlType STRING */ + "segment": string; + }; + measureKeys: "arr" | "mrr"; + dimensionKeys: "region" | "segment"; + }; + } +} +" +`; + +exports[`generateMetricTypeDeclarations — snapshot > emits an empty MetricRegistry interface when no metrics are registered 1`] = ` +"// Auto-generated by AppKit - DO NOT EDIT +// Generated by 'npx @databricks/appkit generate-types' or Vite plugin during build +import "@databricks/appkit-ui/react"; +declare module "@databricks/appkit-ui/react" { + interface MetricRegistry {} +} +" +`; diff --git a/packages/appkit/src/type-generator/tests/metric-registry.test.ts b/packages/appkit/src/type-generator/tests/metric-registry.test.ts new file mode 100644 index 000000000..91b5e0d74 --- /dev/null +++ b/packages/appkit/src/type-generator/tests/metric-registry.test.ts @@ -0,0 +1,285 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { + extractMetricColumns, + generateMetricTypeDeclarations, + parseDescribeTableExtendedJson, + readMetricConfig, + resolveMetricConfig, + syncMetrics, +} from "../metric-registry"; +import type { DatabricksStatementExecutionResponse } from "../types"; + +/** + * Build a representative DESCRIBE TABLE EXTENDED ... AS JSON response. + * + * The Statement Execution API returns one row, one cell — a JSON string + * payload. The shape is broadly: + * + * ```json + * { + * "table_name": "...", + * "columns": [ + * { "name": "arr", "type": "DECIMAL(38,2)", "is_measure": true, "comment": "..." }, + * { "name": "region", "type": "STRING", "is_measure": false } + * ] + * } + * ``` + * + * Phase 1 mocks this. Live integration ships in Phase 7. + */ +function mockDescribeResponse( + payload: unknown, +): DatabricksStatementExecutionResponse { + return { + statement_id: "stmt-mock", + status: { state: "SUCCEEDED" }, + result: { + data_array: [[JSON.stringify(payload)]], + }, + }; +} + +describe("readMetricConfig", () => { + let tmpDir: string; + + beforeEach(async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "appkit-metric-typegen-")); + }); + + afterEach(async () => { + await fs.rm(tmpDir, { recursive: true, force: true }); + }); + + test("returns null when metric.json is absent", async () => { + expect(await readMetricConfig(tmpDir)).toBeNull(); + }); + + test("parses a valid metric.json", async () => { + await fs.writeFile( + path.join(tmpDir, "metric.json"), + JSON.stringify({ sp: { revenue: { source: "demo.public.revenue" } } }), + ); + const cfg = await readMetricConfig(tmpDir); + expect(cfg?.sp?.revenue.source).toBe("demo.public.revenue"); + }); + + test("throws on malformed JSON", async () => { + await fs.writeFile(path.join(tmpDir, "metric.json"), "{not json"); + await expect(readMetricConfig(tmpDir)).rejects.toThrowError( + /parse metric.json/, + ); + }); +}); + +describe("resolveMetricConfig", () => { + test("flattens sp + obo lanes into a sorted entries list", () => { + const cfg = { + sp: { b_metric: { source: "a.b.c" }, a_metric: { source: "a.b.d" } }, + obo: { c_metric: { source: "a.b.e" } }, + }; + const { entries } = resolveMetricConfig(cfg); + expect(entries.map((e) => e.key)).toEqual([ + "a_metric", + "b_metric", + "c_metric", + ]); + expect(entries[0].lane).toBe("sp"); + expect(entries[2].lane).toBe("obo"); + }); + + test("rejects duplicate keys across lanes", () => { + const cfg = { + sp: { revenue: { source: "a.b.c" } }, + obo: { revenue: { source: "a.b.d" } }, + }; + expect(() => resolveMetricConfig(cfg)).toThrowError(/Duplicate metric/); + }); + + test("rejects unknown entry fields", () => { + const cfg = { + sp: { revenue: { source: "a.b.c", cacheTtl: 60 } as any }, + }; + expect(() => resolveMetricConfig(cfg)).toThrowError(/'source' is allowed/); + }); + + test("rejects bad FQN format", () => { + const cfg = { sp: { revenue: { source: "not.three.part.parts" } } }; + expect(() => resolveMetricConfig(cfg)).toThrowError(/three-part UC FQN/); + }); + + test("rejects a metric key starting with a digit", () => { + const cfg = { sp: { "1revenue": { source: "a.b.c" } } }; + expect(() => resolveMetricConfig(cfg)).toThrowError(/Invalid metric key/); + }); +}); + +describe("parseDescribeTableExtendedJson", () => { + test("parses the JSON payload from the first cell", () => { + const response = mockDescribeResponse({ + columns: [{ name: "arr", type: "DECIMAL", is_measure: true }], + }); + const parsed = parseDescribeTableExtendedJson(response); + expect(parsed).toMatchObject({ + columns: [{ name: "arr", type: "DECIMAL", is_measure: true }], + }); + }); + + test("throws on a FAILED status", () => { + expect(() => + parseDescribeTableExtendedJson({ + statement_id: "x", + status: { state: "FAILED", error: { message: "no such table" } }, + }), + ).toThrowError(/no such table/); + }); + + test("throws when the response is empty", () => { + expect(() => + parseDescribeTableExtendedJson({ + statement_id: "x", + status: { state: "SUCCEEDED" }, + result: { data_array: [] }, + }), + ).toThrowError(/no rows/); + }); + + test("throws when the cell is not a JSON string", () => { + expect(() => + parseDescribeTableExtendedJson({ + statement_id: "x", + status: { state: "SUCCEEDED" }, + result: { data_array: [[null]] }, + }), + ).toThrowError(/JSON string/); + }); +}); + +describe("extractMetricColumns", () => { + test("extracts measures and dimensions from the standard shape", () => { + const cols = extractMetricColumns({ + columns: [ + { + name: "arr", + type: "DECIMAL(38,2)", + is_measure: true, + comment: "Annual recurring revenue", + }, + { name: "region", type: "STRING", is_measure: false }, + ], + }); + expect(cols).toHaveLength(2); + expect(cols[0]).toMatchObject({ + name: "arr", + type: "DECIMAL(38,2)", + isMeasure: true, + description: "Annual recurring revenue", + }); + expect(cols[1]).toMatchObject({ + name: "region", + type: "STRING", + isMeasure: false, + }); + }); + + test("falls back to schema.fields shape", () => { + const cols = extractMetricColumns({ + schema: { + fields: [ + { + name: "mrr", + type: { name: "DOUBLE" }, + metadata: { is_measure: true }, + }, + ], + }, + }); + expect(cols).toHaveLength(1); + expect(cols[0]).toMatchObject({ + name: "mrr", + type: "DOUBLE", + isMeasure: true, + }); + }); + + test("returns empty array on unrecognized shape", () => { + expect(extractMetricColumns({ unrelated: true })).toEqual([]); + }); +}); + +describe("syncMetrics", () => { + test("returns one schema per resolved entry, columns split by measure flag", async () => { + const resolution = resolveMetricConfig({ + sp: { revenue: { source: "demo.public.revenue" } }, + }); + + const fetcher = async () => + mockDescribeResponse({ + columns: [ + { name: "arr", type: "DECIMAL(38,2)", is_measure: true }, + { name: "mrr", type: "DECIMAL(38,2)", is_measure: true }, + { name: "region", type: "STRING", is_measure: false }, + ], + }); + + const schemas = await syncMetrics(resolution, fetcher); + expect(schemas).toHaveLength(1); + const [schema] = schemas; + expect(schema.key).toBe("revenue"); + expect(schema.measures.map((m) => m.name)).toEqual(["arr", "mrr"]); + expect(schema.dimensions.map((d) => d.name)).toEqual(["region"]); + }); + + test("falls back to empty columns when DESCRIBE throws (does not crash typegen)", async () => { + const resolution = resolveMetricConfig({ + sp: { revenue: { source: "demo.public.revenue" } }, + }); + + const fetcher = async () => { + throw new Error("warehouse unreachable"); + }; + + const schemas = await syncMetrics(resolution, fetcher); + expect(schemas[0].measures).toEqual([]); + expect(schemas[0].dimensions).toEqual([]); + }); +}); + +describe("generateMetricTypeDeclarations — snapshot", () => { + test("emits a stable MetricRegistry augmentation for a representative input", async () => { + const resolution = resolveMetricConfig({ + sp: { revenue: { source: "appkit_demo.public.revenue_metrics" } }, + }); + + const fetcher = async () => + mockDescribeResponse({ + columns: [ + { + name: "arr", + type: "DECIMAL(38,2)", + is_measure: true, + comment: "Annual recurring revenue", + }, + { + name: "mrr", + type: "DECIMAL(38,2)", + is_measure: true, + comment: "Monthly recurring revenue", + }, + { name: "region", type: "STRING", is_measure: false }, + { name: "segment", type: "STRING", is_measure: false }, + ], + }); + + const schemas = await syncMetrics(resolution, fetcher); + const output = generateMetricTypeDeclarations(schemas); + expect(output).toMatchSnapshot(); + }); + + test("emits an empty MetricRegistry interface when no metrics are registered", () => { + const output = generateMetricTypeDeclarations([]); + expect(output).toMatchSnapshot(); + }); +}); diff --git a/packages/appkit/src/type-generator/vite-plugin.ts b/packages/appkit/src/type-generator/vite-plugin.ts index 5f4a0d4b1..060896331 100644 --- a/packages/appkit/src/type-generator/vite-plugin.ts +++ b/packages/appkit/src/type-generator/vite-plugin.ts @@ -5,6 +5,7 @@ import { createLogger } from "../logging/logger"; import { ANALYTICS_TYPES_FILE, generateFromEntryPoint, + METRIC_TYPES_FILE, TYPES_DIR, } from "./index"; @@ -16,6 +17,8 @@ const logger = createLogger("type-generator:vite-plugin"); interface AppKitTypesPluginOptions { /* Path to the output d.ts file (relative to client folder). */ outFile?: string; + /** Path to the metric registry d.ts file (relative to client folder). */ + metricOutFile?: string; /** Folders to watch for changes. */ watchFolders?: string[]; } @@ -28,6 +31,7 @@ interface AppKitTypesPluginOptions { */ export function appKitTypesPlugin(options?: AppKitTypesPluginOptions): Plugin { let outFile: string; + let metricOutFile: string; let watchFolders: string[]; async function generate() { @@ -44,6 +48,7 @@ export function appKitTypesPlugin(options?: AppKitTypesPluginOptions): Plugin { queryFolder: watchFolders[0], warehouseId, noCache: false, + metricOutFile, }); } catch (error) { // throw in production to fail the build @@ -78,6 +83,10 @@ export function appKitTypesPlugin(options?: AppKitTypesPluginOptions): Plugin { projectRoot, options?.outFile ?? `shared/${TYPES_DIR}/${ANALYTICS_TYPES_FILE}`, ); + metricOutFile = path.resolve( + projectRoot, + options?.metricOutFile ?? `shared/${TYPES_DIR}/${METRIC_TYPES_FILE}`, + ); watchFolders = options?.watchFolders ?? [ path.join(process.cwd(), "config", "queries"), ]; @@ -95,7 +104,10 @@ export function appKitTypesPlugin(options?: AppKitTypesPluginOptions): Plugin { changedFile.startsWith(folder), ); - if (isWatchedFile && changedFile.endsWith(".sql")) { + if ( + isWatchedFile && + (changedFile.endsWith(".sql") || changedFile.endsWith("metric.json")) + ) { generate(); } }); diff --git a/packages/shared/src/schemas/metric-source.generated.ts b/packages/shared/src/schemas/metric-source.generated.ts new file mode 100644 index 000000000..8a5a68eaf --- /dev/null +++ b/packages/shared/src/schemas/metric-source.generated.ts @@ -0,0 +1,43 @@ +// AUTO-GENERATED from metric-source.schema.json — do not edit. +// Run: pnpm exec tsx tools/generate-schema-types.ts +/** + * Metric key. Must be a valid identifier (letters, digits, underscores; cannot start with a digit). Becomes the route key in POST /api/analytics/metric/:key, the hook argument in useMetricView('', ...), and the MetricRegistry augmentation key. + * + * This interface was referenced by `MetricSourceConfiguration`'s JSON-Schema + * via the `definition` "metricKey". + */ +export type MetricKey = string; + +/** + * Schema for AppKit metric.json — declares Unity Catalog Metric View sources for the analytics plugin's metric-view path. Each entry under sp/obo binds a metric key to a UC metric view FQN. Object form (rather than bare string) at v1 enables future per-entry option growth without breaking changes. + */ +export interface MetricSourceConfiguration { + /** + * Reference to the JSON Schema for validation + */ + $schema?: string; + /** + * Metric views queried as the service principal. Cache scope is shared across all users. + */ + sp?: { + [k: string]: MetricEntry; + }; + /** + * Metric views queried as the requesting user (on-behalf-of). Cache scope is per-user. + */ + obo?: { + [k: string]: MetricEntry; + }; +} +/** + * A single metric view source declaration. v1 only accepts the 'source' field; future per-entry options (cacheTtl, defaultFilter, allowlists) ship as additive properties. + * + * This interface was referenced by `MetricSourceConfiguration`'s JSON-Schema + * via the `definition` "metricEntry". + */ +export interface MetricEntry { + /** + * Three-part Unity Catalog FQN of the metric view: .. + */ + source: string; +} diff --git a/packages/shared/src/schemas/metric-source.schema.json b/packages/shared/src/schemas/metric-source.schema.json new file mode 100644 index 000000000..a41ef9679 --- /dev/null +++ b/packages/shared/src/schemas/metric-source.schema.json @@ -0,0 +1,58 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://databricks.github.io/appkit/schemas/metric-source.schema.json", + "title": "AppKit Metric Source Configuration", + "description": "Schema for AppKit metric.json — declares Unity Catalog Metric View sources for the analytics plugin's metric-view path. Each entry under sp/obo binds a metric key to a UC metric view FQN. Object form (rather than bare string) at v1 enables future per-entry option growth without breaking changes.", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Reference to the JSON Schema for validation" + }, + "sp": { + "type": "object", + "description": "Metric views queried as the service principal. Cache scope is shared across all users.", + "additionalProperties": { + "$ref": "#/$defs/metricEntry" + }, + "propertyNames": { + "$ref": "#/$defs/metricKey" + } + }, + "obo": { + "type": "object", + "description": "Metric views queried as the requesting user (on-behalf-of). Cache scope is per-user.", + "additionalProperties": { + "$ref": "#/$defs/metricEntry" + }, + "propertyNames": { + "$ref": "#/$defs/metricKey" + } + } + }, + "additionalProperties": false, + "$defs": { + "metricKey": { + "type": "string", + "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$", + "description": "Metric key. Must be a valid identifier (letters, digits, underscores; cannot start with a digit). Becomes the route key in POST /api/analytics/metric/:key, the hook argument in useMetricView('', ...), and the MetricRegistry augmentation key." + }, + "metricEntry": { + "type": "object", + "description": "A single metric view source declaration. v1 only accepts the 'source' field; future per-entry options (cacheTtl, defaultFilter, allowlists) ship as additive properties.", + "required": ["source"], + "properties": { + "source": { + "type": "string", + "pattern": "^[a-zA-Z0-9_][a-zA-Z0-9_-]*\\.[a-zA-Z0-9_][a-zA-Z0-9_-]*\\.[a-zA-Z0-9_][a-zA-Z0-9_-]*$", + "description": "Three-part Unity Catalog FQN of the metric view: ..", + "examples": [ + "appkit_demo.public.revenue_metrics", + "main.analytics.customer_metrics" + ] + } + }, + "additionalProperties": false + } + } +} diff --git a/packages/shared/src/schemas/metric-source.schema.test.ts b/packages/shared/src/schemas/metric-source.schema.test.ts new file mode 100644 index 000000000..c818490c2 --- /dev/null +++ b/packages/shared/src/schemas/metric-source.schema.test.ts @@ -0,0 +1,108 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import Ajv from "ajv"; +import addFormats from "ajv-formats"; +import { describe, expect, test } from "vitest"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const SCHEMA_PATH = path.join(__dirname, "metric-source.schema.json"); + +function loadValidator() { + const schema = JSON.parse(fs.readFileSync(SCHEMA_PATH, "utf-8")); + const ajv = new Ajv({ allErrors: true, strict: false }); + addFormats(ajv); + return ajv.compile(schema); +} + +describe("metric-source.schema.json", () => { + const validate = loadValidator(); + + test("accepts a valid SP-only configuration", () => { + const config = { + $schema: + "https://databricks.github.io/appkit/schemas/metric-source.schema.json", + sp: { + revenue: { source: "appkit_demo.public.revenue_metrics" }, + }, + obo: {}, + }; + expect(validate(config)).toBe(true); + }); + + test("accepts mixed sp + obo lanes", () => { + const config = { + sp: { revenue: { source: "demo.public.revenue" } }, + obo: { customer: { source: "demo.public.customer_metrics" } }, + }; + expect(validate(config)).toBe(true); + }); + + test("accepts an empty configuration", () => { + expect(validate({})).toBe(true); + expect(validate({ sp: {}, obo: {} })).toBe(true); + }); + + test("rejects a bare-string entry (must be an object)", () => { + const config = { + sp: { revenue: "demo.public.revenue" as any }, + }; + expect(validate(config)).toBe(false); + }); + + test("rejects an entry without source", () => { + const config = { + sp: { revenue: {} }, + }; + expect(validate(config)).toBe(false); + }); + + test("rejects unknown fields on entries", () => { + const config = { + sp: { + revenue: { + source: "demo.public.revenue", + cacheTtl: 60, // future option, not in v1 + }, + }, + }; + expect(validate(config)).toBe(false); + }); + + test("rejects unknown top-level keys", () => { + const config = { + sp: {}, + obo: {}, + unknown: {}, + }; + expect(validate(config)).toBe(false); + }); + + test("rejects a non-three-part FQN", () => { + const cases = [ + "single", + "two.parts", + "four.parts.really.bad", + ".starts.with.dot", + "ends.with.dot.", + ]; + for (const source of cases) { + const config = { sp: { revenue: { source } as any } }; + expect(validate(config)).toBe(false); + } + }); + + test("rejects metric keys that start with a digit", () => { + const config = { + sp: { "1revenue": { source: "demo.public.revenue" } }, + }; + expect(validate(config)).toBe(false); + }); + + test("accepts metric keys with underscores", () => { + const config = { + sp: { customer_metrics: { source: "demo.public.customer_metrics" } }, + }; + expect(validate(config)).toBe(true); + }); +}); diff --git a/tools/generate-schema-types.ts b/tools/generate-schema-types.ts index 18360fb2f..f9f5e7e3d 100644 --- a/tools/generate-schema-types.ts +++ b/tools/generate-schema-types.ts @@ -1,7 +1,11 @@ /** - * Generates TypeScript interfaces from plugin-manifest.schema.json using + * Generates TypeScript interfaces from JSON Schemas using * json-schema-to-typescript. Single source of truth for structural types - * (ResourceFieldEntry, ResourceRequirement, PluginManifest). + * shared between packages. + * + * Currently generates: + * - plugin-manifest.generated.ts (PluginManifest, ResourceRequirement, ...) + * - metric-source.generated.ts (MetricSourceConfiguration) * * Run from repo root: pnpm exec tsx tools/generate-schema-types.ts */ @@ -13,31 +17,63 @@ import { formatWithBiome } from "./format-with-biome.ts"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = path.join(__dirname, ".."); -const SCHEMA_PATH = path.join( - REPO_ROOT, - "packages/shared/src/schemas/plugin-manifest.schema.json", -); -const OUT_PATH = path.join( - REPO_ROOT, - "packages/shared/src/schemas/plugin-manifest.generated.ts", -); - -const BANNER = `// AUTO-GENERATED from plugin-manifest.schema.json — do not edit. + +interface SchemaJob { + schemaPath: string; + outPath: string; + bannerSource: string; + rootRename?: { fromTitle: string; toName: string }; +} + +const JOBS: SchemaJob[] = [ + { + schemaPath: path.join( + REPO_ROOT, + "packages/shared/src/schemas/plugin-manifest.schema.json", + ), + outPath: path.join( + REPO_ROOT, + "packages/shared/src/schemas/plugin-manifest.generated.ts", + ), + bannerSource: "plugin-manifest.schema.json", + rootRename: { + fromTitle: "AppKit Plugin Manifest", + toName: "PluginManifest", + }, + }, + { + schemaPath: path.join( + REPO_ROOT, + "packages/shared/src/schemas/metric-source.schema.json", + ), + outPath: path.join( + REPO_ROOT, + "packages/shared/src/schemas/metric-source.generated.ts", + ), + bannerSource: "metric-source.schema.json", + rootRename: { + fromTitle: "AppKit Metric Source Configuration", + toName: "MetricSourceConfiguration", + }, + }, +]; + +async function compileOne(job: SchemaJob): Promise { + const banner = `// AUTO-GENERATED from ${job.bannerSource} — do not edit. // Run: pnpm exec tsx tools/generate-schema-types.ts `; -async function main(): Promise { - const raw = await compileFromFile(SCHEMA_PATH, { + const raw = await compileFromFile(job.schemaPath, { bannerComment: "", additionalProperties: false, strictIndexSignatures: false, unreachableDefinitions: true, format: false, style: { semi: true, singleQuote: false }, - // Rename the root type (derived from schema title "AppKit Plugin Manifest") - // to "PluginManifest" for ergonomic imports. customName: (schema) => - schema.title === "AppKit Plugin Manifest" ? "PluginManifest" : undefined, + job.rootRename && schema.title === job.rootRename.fromTitle + ? job.rootRename.toName + : undefined, }); // Post-processing: work around json-schema-to-typescript limitations that @@ -45,12 +81,18 @@ async function main(): Promise { // allOf/if-then produces `{ [k: string]: unknown } & { … }` — strip the index-signature part. const output = raw.replace(/\{\s*\[k: string\]: unknown;?\s*\}\s*&\s*/g, ""); - const result = BANNER + output; + const result = banner + output; + + fs.mkdirSync(path.dirname(job.outPath), { recursive: true }); + fs.writeFileSync(job.outPath, result, "utf-8"); + formatWithBiome(job.outPath); + console.log("Wrote", job.outPath); +} - fs.mkdirSync(path.dirname(OUT_PATH), { recursive: true }); - fs.writeFileSync(OUT_PATH, result, "utf-8"); - formatWithBiome(OUT_PATH); - console.log("Wrote", OUT_PATH); +async function main(): Promise { + for (const job of JOBS) { + await compileOne(job); + } } main(); From 3de86dc00a4523f4fec2c1124469dd7503b8e428 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Wed, 29 Apr 2026 23:31:37 +0200 Subject: [PATCH 02/34] =?UTF-8?q?feat(appkit):=20metric=20view=20source=20?= =?UTF-8?q?=E2=80=94=20Phase=202=20dimensions=20and=20time=20grain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend Phase 1's walking skeleton with dimension support and time-grain truncation. The result row narrows at the call site via Pick, M[number] | D[number]>; TimeGrain narrows per-metric-view to YAML-allowed grains. Server: validator rejects unknown dimensions, unknown grains, and timeGrain- without-time-typed-dim. SQL constructor adds GROUP BY ALL when dimensions are present and date_trunc('', ) for time-typed dims when timeGrain is set. Cache key composition extended with sorted dims and grain. Type-gen: extractMetricColumns reads YAML 1.1 time_grain attribute (tolerant parser, accepts list / metadata.time_grain / { grains: [...] } shapes); emits per-metric timeGrains union and @timeGrain JSDoc on time-typed dim entries. Hook: useMetricView widens args with dimensions and timeGrain. Type-level tests (expectTypeOf) cover narrowing across measures- only, dims-only, and combined cases. Tests: 1878 total (95 files), +49 from Phase 1. Backpressure (build, docs, check:fix, typecheck, test, knip) all green. Per prd/analytics-metric-view-source and tasks/.../Phase 2. xavier loop iteration 3. Co-authored-by: Isaac Signed-off-by: Atila Fassina --- .../__tests__/use-metric-view-types.test.ts | 137 +++++ .../hooks/__tests__/use-metric-view.test.ts | 81 ++- packages/appkit-ui/src/react/hooks/index.ts | 2 + packages/appkit-ui/src/react/hooks/types.ts | 83 ++- .../src/react/hooks/use-metric-view.ts | 53 +- .../appkit/src/plugins/analytics/analytics.ts | 2 + .../appkit/src/plugins/analytics/metric.ts | 240 ++++++++- .../plugins/analytics/tests/metric.test.ts | 472 +++++++++++++++++- .../appkit/src/plugins/analytics/types.ts | 39 +- .../src/type-generator/metric-registry.ts | 95 +++- .../metric-registry.test.ts.snap | 32 ++ .../tests/metric-registry.test.ts | 144 ++++++ 12 files changed, 1324 insertions(+), 56 deletions(-) create mode 100644 packages/appkit-ui/src/react/hooks/__tests__/use-metric-view-types.test.ts diff --git a/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view-types.test.ts b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view-types.test.ts new file mode 100644 index 000000000..84455d66b --- /dev/null +++ b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view-types.test.ts @@ -0,0 +1,137 @@ +import { describe, expectTypeOf, test } from "vitest"; +import type { + DimensionKey, + MeasureKey, + TimeGrain, + UseMetricViewArgs, + UseMetricViewRow, +} from "../types"; + +/** + * Compile-time type tests for useMetricView's narrowing behaviour. + * + * These tests use `expectTypeOf` and never invoke the hook at runtime — they + * verify that the registry-derived helpers (`MeasureKey`, `DimensionKey`, + * `TimeGrain`, `UseMetricViewRow`) compose correctly when the registry is + * augmented. + * + * The MetricRegistry interface is augmented locally inside this file via + * module declaration. The augmentation only affects the type universe of + * this test file; production code is untouched. + */ + +declare module "../types" { + interface MetricRegistry { + revenue: { + key: "revenue"; + source: "appkit_demo.public.revenue_metrics"; + lane: "sp"; + measures: { arr: number; mrr: number }; + dimensions: { region: string; segment: string; created_at: string }; + measureKeys: "arr" | "mrr"; + dimensionKeys: "region" | "segment" | "created_at"; + timeGrains: "day" | "month" | "week"; + }; + flat_metric: { + key: "flat_metric"; + source: "demo.public.flat"; + lane: "sp"; + measures: { count: number }; + dimensions: Record; + measureKeys: "count"; + dimensionKeys: never; + timeGrains: never; + }; + } +} + +describe("MeasureKey / DimensionKey / TimeGrain", () => { + test("MeasureKey narrows to the registry's declared measure union", () => { + expectTypeOf>().toEqualTypeOf<"arr" | "mrr">(); + }); + + test("DimensionKey narrows to the registry's declared dimension union", () => { + expectTypeOf>().toEqualTypeOf< + "region" | "segment" | "created_at" + >(); + }); + + test("TimeGrain narrows to the union of YAML-allowed grains", () => { + expectTypeOf>().toEqualTypeOf< + "day" | "month" | "week" + >(); + }); + + test("DimensionKey is `never` when the registry declares no dimensions", () => { + expectTypeOf>().toEqualTypeOf(); + }); + + test("TimeGrain is `never` when the registry declares no time-typed dims", () => { + expectTypeOf>().toEqualTypeOf(); + }); + + test("TimeGrain falls back to `string` for unregistered keys", () => { + type DynamicGrain = TimeGrain; + expectTypeOf().toEqualTypeOf(); + }); +}); + +describe("UseMetricViewArgs — call-site narrowing", () => { + test("measures + dimensions tuples preserve literal types under `const` modifiers", () => { + type Args = UseMetricViewArgs< + "revenue", + readonly ["arr"], + readonly ["region"] + >; + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf< + readonly ["region"] | undefined + >(); + }); + + test("timeGrain is constrained to TimeGrain when provided", () => { + type Args = UseMetricViewArgs< + "revenue", + readonly ["arr"], + readonly ["created_at"] + >; + expectTypeOf().toEqualTypeOf< + "day" | "month" | "week" | undefined + >(); + }); +}); + +describe("UseMetricViewRow — row narrowing via Pick", () => { + test("measures-only call narrows the row to just the chosen measures", () => { + type Row = UseMetricViewRow<"revenue", readonly ["arr"], readonly []>; + expectTypeOf().toEqualTypeOf<{ arr: number }>(); + }); + + test("measures + one dimension narrows to the union of both", () => { + type Row = UseMetricViewRow< + "revenue", + readonly ["arr"], + readonly ["region"] + >; + expectTypeOf().toEqualTypeOf<{ arr: number; region: string }>(); + }); + + test("multiple measures + multiple dimensions composes correctly", () => { + type Row = UseMetricViewRow< + "revenue", + readonly ["arr", "mrr"], + readonly ["region", "created_at"] + >; + expectTypeOf().toEqualTypeOf<{ + arr: number; + mrr: number; + region: string; + created_at: string; + }>(); + }); + + test("dimensions-only call narrows the row to just the dimensions", () => { + type Row = UseMetricViewRow<"revenue", readonly [], readonly ["segment"]>; + expectTypeOf().toEqualTypeOf<{ segment: string }>(); + }); +}); diff --git a/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts index 8f968f567..d1c2e670a 100644 --- a/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts +++ b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts @@ -72,6 +72,82 @@ describe("useMetricView", () => { }); }); + // ── Phase 2: dimensions + timeGrain payload assembly ──────────────────── + test("includes dimensions in the payload when non-empty", () => { + renderHook(() => + useMetricView("revenue", { + measures: ["arr"], + dimensions: ["region", "segment"], + }), + ); + + const payload = JSON.parse( + (mockConnectSSE.mock.calls[0][0] as any).payload, + ); + expect(payload).toEqual({ + measures: ["arr"], + dimensions: ["region", "segment"], + format: "JSON", + }); + }); + + test("omits dimensions from the payload when empty (ungrouped query)", () => { + renderHook(() => + useMetricView("revenue", { measures: ["arr"], dimensions: [] }), + ); + + const payload = JSON.parse( + (mockConnectSSE.mock.calls[0][0] as any).payload, + ); + expect(payload).toEqual({ + measures: ["arr"], + format: "JSON", + }); + expect(payload.dimensions).toBeUndefined(); + }); + + test("includes timeGrain in the payload when provided", () => { + renderHook(() => + useMetricView("revenue", { + measures: ["arr"], + dimensions: ["created_at"], + timeGrain: "month", + }), + ); + + const payload = JSON.parse( + (mockConnectSSE.mock.calls[0][0] as any).payload, + ); + expect(payload).toEqual({ + measures: ["arr"], + dimensions: ["created_at"], + timeGrain: "month", + format: "JSON", + }); + }); + + test("combines dimensions, timeGrain, and limit in the payload", () => { + renderHook(() => + useMetricView("revenue", { + measures: ["arr", "mrr"], + dimensions: ["created_at"], + timeGrain: "week", + limit: 50, + }), + ); + + const payload = JSON.parse( + (mockConnectSSE.mock.calls[0][0] as any).payload, + ); + expect(payload).toEqual({ + measures: ["arr", "mrr"], + dimensions: ["created_at"], + timeGrain: "week", + limit: 50, + format: "JSON", + }); + }); + test("populates data on a result event", async () => { const { result } = renderHook(() => useMetricView("revenue", { measures: ["arr"] }), @@ -149,7 +225,10 @@ describe("useMetricView", () => { test("rejects an empty metric key", () => { expect(() => - renderHook(() => useMetricView("", { measures: ["arr"] } as any)), + // Cast to any so the runtime guard ("non-empty string") is what fails, + // not the compile-time MetricKey union (which is augmented in the + // sibling type-tests file). + renderHook(() => useMetricView("" as any, { measures: ["arr"] } as any)), ).toThrowError(/non-empty string/); }); diff --git a/packages/appkit-ui/src/react/hooks/index.ts b/packages/appkit-ui/src/react/hooks/index.ts index 7fed35ca2..1572de498 100644 --- a/packages/appkit-ui/src/react/hooks/index.ts +++ b/packages/appkit-ui/src/react/hooks/index.ts @@ -14,12 +14,14 @@ export type { QueryRegistry, ServingAlias, ServingEndpointRegistry, + TimeGrain, TypedArrowTable, UseAnalyticsQueryOptions, UseAnalyticsQueryResult, UseMetricViewArgs, UseMetricViewOptions, UseMetricViewResult, + UseMetricViewRow, } from "./types"; export { useAnalyticsQuery } from "./use-analytics-query"; export { diff --git a/packages/appkit-ui/src/react/hooks/types.ts b/packages/appkit-ui/src/react/hooks/types.ts index 086661e7a..009f36410 100644 --- a/packages/appkit-ui/src/react/hooks/types.ts +++ b/packages/appkit-ui/src/react/hooks/types.ts @@ -141,7 +141,7 @@ export interface ServingClientConfig { } // ============================================================================ -// Metric View Registry (Phase 1 — measures only) +// Metric View Registry (Phase 2 — measures + dimensions + time grain) // ============================================================================ /** @@ -161,9 +161,10 @@ export interface ServingClientConfig { * source: "appkit_demo.public.revenue_metrics"; * lane: "sp"; * measures: { arr: number; mrr: number }; - * dimensions: Record; + * dimensions: { region: string; created_at: string }; * measureKeys: "arr" | "mrr"; - * dimensionKeys: never; + * dimensionKeys: "region" | "created_at"; + * timeGrains: "day" | "week" | "month"; * }; * } * } @@ -195,6 +196,20 @@ export type DimensionKey = K extends AugmentedRegistry : never : never; +/** + * The union of allowed time-grains for a registered metric key — derived from + * the YAML 1.1 `time_grain` attributes on time-typed dimensions. Resolves to + * `string` for unregistered keys (so dynamic callers don't compile-error) and + * to `never` for registered metrics that have zero time-typed dimensions. + */ +export type TimeGrain = K extends AugmentedRegistry + ? MetricRegistry[K] extends { timeGrains: infer G } + ? G extends string + ? G + : never + : never + : string; + /** The "measures" entry on a registered metric — a record of name → row type. */ type MetricMeasureMap = K extends AugmentedRegistry ? MetricRegistry[K] extends { measures: infer M } @@ -216,14 +231,66 @@ type MetricDimensionMap = K extends AugmentedRegistry /** Full result row type for a registered metric (measures + dimensions). */ export type MetricRow = MetricMeasureMap & MetricDimensionMap; -/** Phase 1 args: only measures are accepted. */ -export interface UseMetricViewArgs { - measures: ReadonlyArray>; +/** + * Phase 2 args: measures + dimensions + optional time grain. + * + * Generics: + * - `K` — the metric key (narrows to the registry literal at the call site). + * - `M` — the chosen measure tuple (narrows to the literal subset). + * - `D` — the chosen dimension tuple (narrows to the literal subset). + * + * Use `const` modifiers on `M` and `D` at the call site for literal-preserving + * inference (matches the Phase 1 measures-only pattern): + * + * ```tsx + * useMetricView("revenue", { + * measures: ["arr"] as const, + * dimensions: ["region", "created_at"] as const, + * timeGrain: "month", + * }); + * ``` + */ +export interface UseMetricViewArgs< + K extends MetricKey, + M extends ReadonlyArray> = ReadonlyArray>, + D extends ReadonlyArray> = ReadonlyArray>, +> { + measures: M; + /** + * Dimensions to GROUP BY. Empty (or omitted) → ungrouped query. Only + * dimensions declared on the metric view are accepted. + */ + dimensions?: D; + /** + * Time-grain truncation applied to every time-typed dimension in + * `dimensions`. Narrows to the union of grains the metric view declares. + * + * If the metric view has no time-typed dimensions, this field cannot be set + * (the type resolves to `never`). + * + * Setting `timeGrain` without including any time-typed dimension in + * `dimensions` is a server-side 400. + */ + timeGrain?: TimeGrain; /** Optional row cap. */ limit?: number; } -/** Phase 1 options: format passthrough + autoStart toggle. */ +/** + * Row narrowing helper: produce the row type containing only the chosen + * measures and dimensions, matching what the server projects. + * + * If callers omit dimensions, the row contains only measures; if callers omit + * measures (not allowed at v1, but the type stays sound), the row contains + * only dimensions. + */ +export type UseMetricViewRow< + K extends MetricKey, + M extends ReadonlyArray>, + D extends ReadonlyArray>, +> = Pick, (M[number] | D[number]) & keyof MetricRow>; + +/** Phase 2 options: format passthrough + autoStart toggle. */ export interface UseMetricViewOptions { format?: F; /** Whether to fire the request automatically on mount. Default: true. */ @@ -232,7 +299,7 @@ export interface UseMetricViewOptions { maxParametersSize?: number; } -/** Phase 1 result shape: { data, loading, error }. */ +/** Phase 2 result shape: { data, loading, error }. */ export interface UseMetricViewResult { data: TRow[] | null; loading: boolean; diff --git a/packages/appkit-ui/src/react/hooks/use-metric-view.ts b/packages/appkit-ui/src/react/hooks/use-metric-view.ts index ee5b15c98..a67d0c509 100644 --- a/packages/appkit-ui/src/react/hooks/use-metric-view.ts +++ b/packages/appkit-ui/src/react/hooks/use-metric-view.ts @@ -2,36 +2,49 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { connectSSE } from "@/js"; import type { AnalyticsFormat, + DimensionKey, + MeasureKey, MetricKey, - MetricRow, UseMetricViewArgs, UseMetricViewOptions, UseMetricViewResult, + UseMetricViewRow, } from "./types"; /** * Subscribe to a metric-view query over SSE. * - * Phase 1 surface — accepts `{ measures }` only. Phase 2/3 widen to - * `dimensions`, `filter`, `timeGrain`. The hook signature mirrors - * `useAnalyticsQuery`'s shape so that adopters muscle-memorize the call - * pattern across the two hooks. + * Phase 2 surface — accepts `{ measures, dimensions?, timeGrain?, limit? }`. + * The result row type narrows at the call site to + * `Pick, M[number] | D[number]>` based on the chosen measures + * and dimensions, so chart code receives the exact shape it asked for. + * + * Use `as const` on the `measures` and `dimensions` arrays at the call site + * to preserve literal types (the same pattern used elsewhere in AppKit for + * registry-narrowed APIs). * * @example * ```tsx * const { data, loading, error } = useMetricView("revenue", { - * measures: ["arr"], + * measures: ["arr"] as const, + * dimensions: ["region", "created_at"] as const, + * timeGrain: "month", * }); + * // data: Array<{ arr: number; region: string; created_at: string }> | null * ``` */ export function useMetricView< K extends MetricKey = MetricKey, + const M extends ReadonlyArray> = ReadonlyArray>, + const D extends ReadonlyArray> = ReadonlyArray< + DimensionKey + >, F extends AnalyticsFormat = "JSON", >( metricKey: K, - args: UseMetricViewArgs, + args: UseMetricViewArgs, options: UseMetricViewOptions = {} as UseMetricViewOptions, -): UseMetricViewResult> { +): UseMetricViewResult> { if (!metricKey || metricKey.trim().length === 0) { throw new Error("useMetricView: 'metricKey' must be a non-empty string."); } @@ -42,7 +55,7 @@ export function useMetricView< const url = `/api/analytics/metric/${encodeURIComponent(metricKey)}`; - type ResultType = MetricRow; + type ResultType = UseMetricViewRow; const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -50,11 +63,20 @@ export function useMetricView< const payload = useMemo(() => { try { - const body = { + const dimensions = args.dimensions ? [...args.dimensions] : undefined; + const body: Record = { measures: [...args.measures], - ...(typeof args.limit === "number" ? { limit: args.limit } : {}), format, }; + if (dimensions && dimensions.length > 0) { + body.dimensions = dimensions; + } + if (typeof args.timeGrain === "string" && args.timeGrain.length > 0) { + body.timeGrain = args.timeGrain; + } + if (typeof args.limit === "number") { + body.limit = args.limit; + } const serialized = JSON.stringify(body); const sizeInBytes = new Blob([serialized]).size; if (sizeInBytes > maxParametersSize) { @@ -67,7 +89,14 @@ export function useMetricView< console.error("useMetricView: Failed to serialize request body", err); return null; } - }, [args.measures, args.limit, format, maxParametersSize]); + }, [ + args.measures, + args.dimensions, + args.timeGrain, + args.limit, + format, + maxParametersSize, + ]); const start = useCallback(() => { if (payload === null) { diff --git a/packages/appkit/src/plugins/analytics/analytics.ts b/packages/appkit/src/plugins/analytics/analytics.ts index 95dd8d50a..25d805798 100644 --- a/packages/appkit/src/plugins/analytics/analytics.ts +++ b/packages/appkit/src/plugins/analytics/analytics.ts @@ -326,6 +326,8 @@ export class AnalyticsPlugin extends Plugin { const cacheKey = composeMetricCacheKey({ metricKey: key, measures: request.measures, + dimensions: request.dimensions, + timeGrain: request.timeGrain, format, executorKey, limit: request.limit, diff --git a/packages/appkit/src/plugins/analytics/metric.ts b/packages/appkit/src/plugins/analytics/metric.ts index 14a4a2226..1db959101 100644 --- a/packages/appkit/src/plugins/analytics/metric.ts +++ b/packages/appkit/src/plugins/analytics/metric.ts @@ -54,6 +54,25 @@ const metricConfigSchema = z }) .strict(); +/** + * Per-metric metadata threaded from the type-generator into the runtime + * registry. Phase 1 supplied measures + dimensions; Phase 2 adds the + * per-dim time-grain map for time-typed dimensions. + * + * Internal to this module — the type-generator wires the JSON metadata blob + * (Phase 5) into `loadMetricRegistry` via the inferred function parameter + * shape, so external consumers never name this interface directly. + */ +interface MetricBuildTimeMetadata { + measures?: string[]; + dimensions?: string[]; + /** + * Dimension name → allowed time-grains. Only populated for time-typed + * dimensions; regular dimensions are absent from this map. + */ + timeGrainsByDim?: Record; +} + /** * Read and validate `config/queries/metric.json`. * @@ -66,7 +85,7 @@ const metricConfigSchema = z * structural checks. */ export async function loadMetricRegistry( - metadata?: Record, + metadata?: Record, queriesDir: string = QUERIES_DIR, ): Promise> { const metricPath = path.join(queriesDir, METRIC_CONFIG_FILE); @@ -118,6 +137,7 @@ export async function loadMetricRegistry( lane, knownMeasures: meta?.measures ?? [], knownDimensions: meta?.dimensions ?? [], + knownTimeGrainsByDim: meta?.timeGrainsByDim ?? {}, }; } } @@ -137,7 +157,15 @@ export async function loadMetricRegistry( * metadata available) any non-empty string is accepted and validation defers * to the warehouse. * - * Phase 1 body shape: `{ measures, format?, limit? }`. Phase 2/3 widen this. + * Phase 2 body shape: `{ measures, dimensions?, timeGrain?, format?, limit? }`. + * + * Validation matrix: + * - `measures` — must be a non-empty array; constrained to `knownMeasures` + * when build-time metadata is available. + * - `dimensions` — optional array; constrained to `knownDimensions`. + * - `timeGrain` — optional string; constrained to the union of grains + * declared across all time-typed dimensions; rejected unless the + * `dimensions` array contains at least one time-typed dimension. */ export function makeMetricRequestSchema( registration: MetricRegistration, @@ -160,11 +188,41 @@ export function makeMetricRequestSchema( ) : baseMeasureSchema; - return z + const knownDimensions = registration.knownDimensions; + const baseDimensionSchema = z + .string() + .min(1, { message: "dimension name cannot be empty" }); + const dimensionItemSchema = + knownDimensions.length > 0 + ? baseDimensionSchema.refine( + (name: string) => knownDimensions.includes(name), + { + message: `dimension must be one of: ${knownDimensions.join(", ")}`, + }, + ) + : baseDimensionSchema; + + // Aggregate the union of grains the metric view supports. Empty union means + // no time-typed dimensions are declared — `timeGrain` cannot be set. + const grainsByDim = registration.knownTimeGrainsByDim; + const allowedGrains = collectAllowedGrains(grainsByDim); + const baseTimeGrainSchema = z + .string() + .min(1, { message: "timeGrain cannot be empty" }); + const timeGrainSchema = + allowedGrains.length > 0 + ? baseTimeGrainSchema.refine((g: string) => allowedGrains.includes(g), { + message: `timeGrain must be one of: ${allowedGrains.join(", ")}`, + }) + : baseTimeGrainSchema; + + const baseObject = z .object({ measures: z .array(measureItemSchema) .min(1, { message: "measures must contain at least one entry" }), + dimensions: z.array(dimensionItemSchema).optional(), + timeGrain: timeGrainSchema.optional(), format: z.enum(["JSON", "ARROW"]).optional(), limit: z .number() @@ -172,7 +230,42 @@ export function makeMetricRequestSchema( .positive({ message: "limit must be positive" }) .optional(), }) - .strict() as z.ZodType; + .strict(); + + // Cross-field rule: timeGrain is meaningless without a time-typed dimension + // in the dimensions list. Failing fast here keeps the SQL constructor + // honest (no `date_trunc(, )` without a real column to truncate). + return baseObject.superRefine((value, ctx) => { + if (value.timeGrain == null) return; + const dims = value.dimensions ?? []; + const hasTimeDim = dims.some( + (d) => Array.isArray(grainsByDim[d]) && grainsByDim[d].length > 0, + ); + if (!hasTimeDim) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["timeGrain"], + message: + "timeGrain specified but no time-typed dimension is included in 'dimensions'", + }); + } + }) as z.ZodType; +} + +/** + * Aggregate the set of allowed time-grains across every time-typed dimension. + * + * Sorted + deduplicated so the validator's error messages and the cache-key + * construction are deterministic. + */ +function collectAllowedGrains(grainsByDim: Record): string[] { + const set = new Set(); + for (const grains of Object.values(grainsByDim)) { + for (const g of grains) { + set.add(g); + } + } + return [...set].sort(); } /** @@ -227,13 +320,43 @@ function assertSafeFqn(fqn: string): void { const MEASURE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/; /** - * Construct the Phase 1 metric SQL. + * Dimension name pattern. Matches the identifier shape we accept for measures + * — column references cannot be parameterized in SQL, so they must be + * conservatively safe identifiers (no spaces, no quotes, no SQL operators). + */ +const DIMENSION_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/; + +/** + * Time-grain enum values that are safe to interpolate into `date_trunc()`. + * The build-time metadata supplies these as YAML 1.1 lowercase tokens — we + * only accept that shape; anything else (mixed case, quoted strings, + * SQL operators) is rejected before reaching the SQL string. + */ +const TIME_GRAIN_PATTERN = /^[a-z][a-z_]*$/; + +/** + * Construct the Phase 2 metric SQL. + * + * Shape: * - * Shape: `SELECT MEASURE(m1), MEASURE(m2) FROM [LIMIT n]`. + * SELECT MEASURE(m), date_trunc('', ) AS , + * FROM + * [GROUP BY ALL] + * [LIMIT n] * - * Phase 1 has no dimensions, no filter, no GROUP BY, no time-grain. Each of - * those is a follow-on phase with its own dedicated test surface. The intent - * here is the integration spine, not a feature-rich generator. + * Notes: + * - All column references (measures, dimensions) are validated against the + * registry's `knownMeasures` / `knownDimensions` and against the conservative + * identifier pattern. No user-supplied string flows into the SQL string + * without passing both gates. + * - `date_trunc('', col) AS col` is emitted for every time-typed + * dimension when `timeGrain` is set. The grain literal is single-quoted in + * the SQL — we cannot use a bind variable for `date_trunc`'s first + * argument, so we restrict to the registry's allowed grain enum. + * - `GROUP BY ALL` is added when at least one dimension is requested. UC + * requires GROUP BY when MEASURE() is mixed with non-aggregated columns; + * `GROUP BY ALL` is the documented form that works without re-listing each + * dimension. */ export function buildMetricSql( registration: MetricRegistration, @@ -261,44 +384,129 @@ export function buildMetricSql( } } + const dimensions = request.dimensions ?? []; + for (const d of dimensions) { + if (!DIMENSION_NAME_PATTERN.test(d)) { + throw new Error( + `Refusing to build SQL: dimension "${d}" is not a valid identifier.`, + ); + } + if ( + registration.knownDimensions.length > 0 && + !registration.knownDimensions.includes(d) + ) { + throw new Error( + `Refusing to build SQL: unknown dimension "${d}" for metric "${registration.key}".`, + ); + } + } + + if (request.timeGrain !== undefined) { + if (!TIME_GRAIN_PATTERN.test(request.timeGrain)) { + throw new Error( + `Refusing to build SQL: timeGrain "${request.timeGrain}" is not a valid grain token.`, + ); + } + const allowed = collectAllowedGrains(registration.knownTimeGrainsByDim); + if (allowed.length > 0 && !allowed.includes(request.timeGrain)) { + throw new Error( + `Refusing to build SQL: unknown timeGrain "${request.timeGrain}" for metric "${registration.key}".`, + ); + } + const hasTimeDim = dimensions.some((d) => isTimeTypedDim(registration, d)); + if (!hasTimeDim) { + throw new Error( + `Refusing to build SQL: timeGrain "${request.timeGrain}" set but no time-typed dimension is in 'dimensions'.`, + ); + } + } + // Deterministic order so cache keys collapse semantically equivalent calls. // Sort-before-hash composition is finalized in Phase 4; sorting the SELECT // list here is the same idea applied to the SQL itself. const measureClauses = [...request.measures] .sort() - .map((m) => `MEASURE(${m})`) - .join(", "); + .map((m) => `MEASURE(${m})`); + + const dimensionClauses = [...dimensions] + .sort() + .map((d) => renderDimensionClause(registration, d, request.timeGrain)); + + const selectList = [...measureClauses, ...dimensionClauses].join(", "); + const groupByClause = dimensions.length > 0 ? " GROUP BY ALL" : ""; const limitClause = typeof request.limit === "number" && request.limit > 0 ? ` LIMIT ${Math.floor(request.limit)}` : ""; - const statement = `SELECT ${measureClauses} FROM ${registration.source}${limitClause}`; + const statement = `SELECT ${selectList} FROM ${registration.source}${groupByClause}${limitClause}`; return { statement, parameters: [] }; } /** - * Compose the Phase 1 cache key. + * Whether a dimension name is registered as time-typed (carries a non-empty + * `time_grain` attribute in the YAML). + */ +function isTimeTypedDim( + registration: MetricRegistration, + dim: string, +): boolean { + const grains = registration.knownTimeGrainsByDim[dim]; + return Array.isArray(grains) && grains.length > 0; +} + +/** + * Render a single SELECT-list clause for a dimension. + * + * Time-typed dimensions are wrapped in `date_trunc('', ) AS ` + * when `timeGrain` is set; non-time dimensions render as the bare column name. + * + * The grain literal is whitelisted by `collectAllowedGrains(registration)` and + * the column name has already passed the identifier-pattern guard above, so + * neither flows through user-controlled bytes. + */ +function renderDimensionClause( + registration: MetricRegistration, + dim: string, + timeGrain: string | undefined, +): string { + if (timeGrain && isTimeTypedDim(registration, dim)) { + return `date_trunc('${timeGrain}', ${dim}) AS ${dim}`; + } + return dim; +} + +/** + * Compose the cache key. * * Reserved namespace `metric:` separates metric-view caches from query - * caches. Phase 4 finalizes sort-before-hash composition; Phase 1 only needs - * the namespace to be reserved + a stable per-key/per-args/per-executor key - * so the cache test surface works. + * caches. Phase 4 finalizes sort-before-hash composition with the full + * argsHash / executorKey discipline; Phase 2's incremental need is for the + * key to vary on dimensions + timeGrain so semantically distinct calls get + * distinct cache entries. + * + * Order-insensitive components (measures, dimensions) are sorted before + * hashing into the key string, matching the PRD's sort-before-hash invariant. */ export function composeMetricCacheKey(input: { metricKey: string; measures: string[]; + dimensions?: string[]; + timeGrain?: string; format: string; executorKey: string; limit?: number; }): string[] { const sortedMeasures = [...input.measures].sort(); + const sortedDimensions = [...(input.dimensions ?? [])].sort(); return [ "metric", input.metricKey, input.format, sortedMeasures.join(","), + sortedDimensions.join(","), + input.timeGrain ?? "_", typeof input.limit === "number" ? String(input.limit) : "_", input.executorKey, ]; diff --git a/packages/appkit/src/plugins/analytics/tests/metric.test.ts b/packages/appkit/src/plugins/analytics/tests/metric.test.ts index 9dc670034..52d711469 100644 --- a/packages/appkit/src/plugins/analytics/tests/metric.test.ts +++ b/packages/appkit/src/plugins/analytics/tests/metric.test.ts @@ -63,7 +63,10 @@ const REVENUE_REGISTRATION: MetricRegistration = { source: "appkit_demo.public.revenue_metrics", lane: "sp", knownMeasures: ["arr", "mrr"], - knownDimensions: ["region", "segment"], + knownDimensions: ["region", "segment", "created_at"], + knownTimeGrainsByDim: { + created_at: ["day", "month", "week"], + }, }; describe("metric — pure helpers", () => { @@ -113,8 +116,9 @@ describe("metric — pure helpers", () => { expect(() => validateMetricRequest(REVENUE_REGISTRATION, { measures: ["arr"], - dimensions: ["region"], // not allowed at v1 - }), + // 'filter' is reserved for Phase 3; the strict() schema must reject it. + filter: [{ member: "region", operator: "in", values: ["EMEA"] }], + } as any), ).toThrowError(); }); @@ -135,6 +139,99 @@ describe("metric — pure helpers", () => { expect(a.safeParse({ measures: ["arr"] }).success).toBe(true); expect(b.safeParse({ measures: ["arr"] }).success).toBe(true); }); + + // ── Phase 2: dimensions ───────────────────────────────────────────── + test("accepts a request with known dimensions", () => { + const parsed = validateMetricRequest(REVENUE_REGISTRATION, { + measures: ["arr"], + dimensions: ["region"], + }); + expect(parsed.dimensions).toEqual(["region"]); + }); + + test("accepts an empty dimensions array (ungrouped)", () => { + const parsed = validateMetricRequest(REVENUE_REGISTRATION, { + measures: ["arr"], + dimensions: [], + }); + expect(parsed.dimensions).toEqual([]); + }); + + test("rejects an unknown dimension with a clear error", () => { + expect(() => + validateMetricRequest(REVENUE_REGISTRATION, { + measures: ["arr"], + dimensions: ["nonexistent"], + }), + ).toThrowError(/dimensions\.0/); + }); + + test("falls open on dimensions when knownDimensions is empty", () => { + const looseRegistration: MetricRegistration = { + ...REVENUE_REGISTRATION, + knownDimensions: [], + knownTimeGrainsByDim: {}, + }; + const parsed = validateMetricRequest(looseRegistration, { + measures: ["arr"], + dimensions: ["any_column"], + }); + expect(parsed.dimensions).toEqual(["any_column"]); + }); + + // ── Phase 2: time grain ───────────────────────────────────────────── + test("accepts a known timeGrain when a time-typed dim is present", () => { + const parsed = validateMetricRequest(REVENUE_REGISTRATION, { + measures: ["arr"], + dimensions: ["created_at"], + timeGrain: "month", + }); + expect(parsed.timeGrain).toBe("month"); + }); + + test("rejects a timeGrain not in the metric's allowed enum", () => { + expect(() => + validateMetricRequest(REVENUE_REGISTRATION, { + measures: ["arr"], + dimensions: ["created_at"], + timeGrain: "year", + }), + ).toThrowError(/timeGrain must be one of/); + }); + + test("rejects timeGrain when no time-typed dim is in dimensions", () => { + expect(() => + validateMetricRequest(REVENUE_REGISTRATION, { + measures: ["arr"], + dimensions: ["region"], + timeGrain: "month", + }), + ).toThrowError(/no time-typed dimension/); + }); + + test("rejects timeGrain when dimensions is omitted entirely", () => { + expect(() => + validateMetricRequest(REVENUE_REGISTRATION, { + measures: ["arr"], + timeGrain: "month", + }), + ).toThrowError(/no time-typed dimension/); + }); + + test("rejects timeGrain when the metric view has no time-typed dims", () => { + const noTimeRegistration: MetricRegistration = { + ...REVENUE_REGISTRATION, + knownDimensions: ["region", "segment"], + knownTimeGrainsByDim: {}, + }; + expect(() => + validateMetricRequest(noTimeRegistration, { + measures: ["arr"], + dimensions: ["region"], + timeGrain: "month", + }), + ).toThrowError(); + }); }); describe("buildMetricSql", () => { @@ -203,6 +300,125 @@ describe("metric — pure helpers", () => { buildMetricSql(REVENUE_REGISTRATION, { measures: [] }), ).toThrowError(/at least one measure/); }); + + // ── Phase 2: dimensions + GROUP BY ALL ────────────────────────────── + test("emits GROUP BY ALL when dimensions are present (snapshot — measures-only Phase 1 case)", () => { + const { statement } = buildMetricSql(REVENUE_REGISTRATION, { + measures: ["arr"], + }); + expect(statement).toMatchInlineSnapshot( + `"SELECT MEASURE(arr) FROM appkit_demo.public.revenue_metrics"`, + ); + }); + + test("emits dimensions + GROUP BY ALL (snapshot — dims-only)", () => { + const { statement } = buildMetricSql(REVENUE_REGISTRATION, { + measures: ["arr"], + dimensions: ["region"], + }); + expect(statement).toMatchInlineSnapshot( + `"SELECT MEASURE(arr), region FROM appkit_demo.public.revenue_metrics GROUP BY ALL"`, + ); + }); + + test("emits date_trunc for time-typed dim with timeGrain (snapshot — dims+time-grain)", () => { + const { statement } = buildMetricSql(REVENUE_REGISTRATION, { + measures: ["arr"], + dimensions: ["created_at", "region"], + timeGrain: "month", + }); + expect(statement).toMatchInlineSnapshot( + `"SELECT MEASURE(arr), date_trunc('month', created_at) AS created_at, region FROM appkit_demo.public.revenue_metrics GROUP BY ALL"`, + ); + }); + + test("emits dims + time-grain + limit together (snapshot — dims+time-grain+limit)", () => { + const { statement } = buildMetricSql(REVENUE_REGISTRATION, { + measures: ["arr", "mrr"], + dimensions: ["created_at"], + timeGrain: "week", + limit: 50, + }); + expect(statement).toMatchInlineSnapshot( + `"SELECT MEASURE(arr), MEASURE(mrr), date_trunc('week', created_at) AS created_at FROM appkit_demo.public.revenue_metrics GROUP BY ALL LIMIT 50"`, + ); + }); + + test("does not wrap regular (non-time) dims in date_trunc when timeGrain is set", () => { + const { statement } = buildMetricSql(REVENUE_REGISTRATION, { + measures: ["arr"], + dimensions: ["region", "created_at"], + timeGrain: "day", + }); + // Only created_at is wrapped; region renders as the bare column. + expect(statement).toContain("date_trunc('day', created_at)"); + expect(statement).toContain(", region"); + expect(statement).not.toContain("date_trunc('day', region)"); + }); + + test("rejects unknown dimensions (defense in depth past the validator)", () => { + expect(() => + buildMetricSql(REVENUE_REGISTRATION, { + measures: ["arr"], + dimensions: ["nonexistent"], + }), + ).toThrowError(/unknown dimension/i); + }); + + test("rejects dimensions that are not valid identifiers", () => { + const looseRegistration: MetricRegistration = { + ...REVENUE_REGISTRATION, + knownDimensions: [], + knownTimeGrainsByDim: {}, + }; + expect(() => + buildMetricSql(looseRegistration, { + measures: ["arr"], + dimensions: ["region; DROP TABLE foo --"], + }), + ).toThrowError(/not a valid identifier/); + }); + + test("rejects unknown timeGrain values", () => { + expect(() => + buildMetricSql(REVENUE_REGISTRATION, { + measures: ["arr"], + dimensions: ["created_at"], + timeGrain: "year", + }), + ).toThrowError(/unknown timeGrain/i); + }); + + test("rejects timeGrain when no time-typed dim is in dimensions", () => { + expect(() => + buildMetricSql(REVENUE_REGISTRATION, { + measures: ["arr"], + dimensions: ["region"], + timeGrain: "month", + }), + ).toThrowError(/no time-typed dimension/); + }); + + test("rejects timeGrain values that do not match the safe token shape", () => { + expect(() => + buildMetricSql(REVENUE_REGISTRATION, { + measures: ["arr"], + dimensions: ["created_at"], + timeGrain: "Month' OR 1=1 --", + }), + ).toThrowError(/not a valid grain token/); + }); + + test("sorts dimensions lexicographically for deterministic SQL", () => { + const { statement } = buildMetricSql(REVENUE_REGISTRATION, { + measures: ["arr"], + dimensions: ["segment", "region"], + }); + // region comes before segment alphabetically. + expect(statement).toBe( + "SELECT MEASURE(arr), region, segment FROM appkit_demo.public.revenue_metrics GROUP BY ALL", + ); + }); }); describe("composeMetricCacheKey", () => { @@ -265,6 +481,82 @@ describe("metric — pure helpers", () => { }); expect(a).not.toEqual(b); }); + + // ── Phase 2: dimensions + timeGrain ───────────────────────────────── + test("normalizes dimension order for cache hits across equivalent calls", () => { + const a = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + dimensions: ["region", "segment"], + format: "JSON", + executorKey: "sp", + }); + const b = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + dimensions: ["segment", "region"], + format: "JSON", + executorKey: "sp", + }); + expect(a).toEqual(b); + }); + + test("differentiates calls with different dimensions", () => { + const a = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + dimensions: ["region"], + format: "JSON", + executorKey: "sp", + }); + const b = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + dimensions: ["segment"], + format: "JSON", + executorKey: "sp", + }); + expect(a).not.toEqual(b); + }); + + test("differentiates calls with different timeGrain values", () => { + const a = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + dimensions: ["created_at"], + timeGrain: "day", + format: "JSON", + executorKey: "sp", + }); + const b = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + dimensions: ["created_at"], + timeGrain: "month", + format: "JSON", + executorKey: "sp", + }); + expect(a).not.toEqual(b); + }); + + test("differentiates a request with timeGrain from one without", () => { + const withGrain = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + dimensions: ["created_at"], + timeGrain: "day", + format: "JSON", + executorKey: "sp", + }); + const withoutGrain = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + dimensions: ["created_at"], + format: "JSON", + executorKey: "sp", + }); + expect(withGrain).not.toEqual(withoutGrain); + }); }); }); @@ -304,6 +596,7 @@ describe("loadMetricRegistry", () => { lane: "sp", knownMeasures: [], knownDimensions: [], + knownTimeGrainsByDim: {}, }); }); @@ -324,6 +617,30 @@ describe("loadMetricRegistry", () => { expect(registry.revenue.knownDimensions).toEqual(["region"]); }); + test("merges build-time time-grain metadata into knownTimeGrainsByDim", async () => { + const fs = await import("node:fs/promises"); + const path = await import("node:path"); + await fs.writeFile( + path.join(tmpDir, "metric.json"), + JSON.stringify({ + sp: { revenue: { source: "demo.public.revenue" } }, + }), + ); + const registry = await loadMetricRegistry( + { + revenue: { + measures: ["arr"], + dimensions: ["region", "created_at"], + timeGrainsByDim: { created_at: ["day", "month"] }, + }, + }, + tmpDir, + ); + expect(registry.revenue.knownTimeGrainsByDim).toEqual({ + created_at: ["day", "month"], + }); + }); + test("rejects unknown fields on entries (strict)", async () => { const fs = await import("node:fs/promises"); const path = await import("node:path"); @@ -564,4 +881,153 @@ describe("AnalyticsPlugin — metric route handler", () => { expect(executeMock).toHaveBeenCalledTimes(1); }); + + // ── Phase 2: dimensions + time grain via the full route ─────────────── + test("constructs GROUP BY ALL SQL when dimensions are requested", async () => { + const plugin = new AnalyticsPlugin(config); + plugin._setMetricRegistryForTesting({ + revenue: REVENUE_REGISTRATION, + }); + const { router, getHandler } = createMockRouter(); + + const executeMock = vi.fn().mockResolvedValue({ + result: { data: [{ arr: 1, region: "EMEA" }] }, + }); + (plugin as any).SQLClient.executeStatement = executeMock; + + plugin.injectRoutes(router); + + const handler = getHandler("POST", "/metric/:key"); + const mockReq = createMockRequest({ + params: { key: "revenue" }, + body: { measures: ["arr"], dimensions: ["region"] }, + }); + const mockRes = createMockResponse(); + + await handler(mockReq, mockRes); + + expect(executeMock).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + statement: + "SELECT MEASURE(arr), region FROM appkit_demo.public.revenue_metrics GROUP BY ALL", + }), + expect.any(AbortSignal), + ); + }); + + test("constructs date_trunc SQL when timeGrain is set on a time-typed dim", async () => { + const plugin = new AnalyticsPlugin(config); + plugin._setMetricRegistryForTesting({ + revenue: REVENUE_REGISTRATION, + }); + const { router, getHandler } = createMockRouter(); + + const executeMock = vi.fn().mockResolvedValue({ + result: { data: [{ arr: 1, created_at: "2026-01-01" }] }, + }); + (plugin as any).SQLClient.executeStatement = executeMock; + + plugin.injectRoutes(router); + + const handler = getHandler("POST", "/metric/:key"); + const mockReq = createMockRequest({ + params: { key: "revenue" }, + body: { + measures: ["arr"], + dimensions: ["created_at"], + timeGrain: "month", + }, + }); + const mockRes = createMockResponse(); + + await handler(mockReq, mockRes); + + expect(executeMock).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + statement: + "SELECT MEASURE(arr), date_trunc('month', created_at) AS created_at FROM appkit_demo.public.revenue_metrics GROUP BY ALL", + }), + expect.any(AbortSignal), + ); + }); + + test("returns 400 when timeGrain is requested but no time-typed dim is grouped", async () => { + const plugin = new AnalyticsPlugin(config); + plugin._setMetricRegistryForTesting({ + revenue: REVENUE_REGISTRATION, + }); + const { router, getHandler } = createMockRouter(); + + plugin.injectRoutes(router); + + const handler = getHandler("POST", "/metric/:key"); + const mockReq = createMockRequest({ + params: { key: "revenue" }, + body: { + measures: ["arr"], + dimensions: ["region"], + timeGrain: "month", + }, + }); + const mockRes = createMockResponse(); + + await handler(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(400); + const errorPayload = (mockRes.json as any).mock.calls[0][0]; + expect(errorPayload.error).toMatch(/no time-typed dimension/); + expect(errorPayload.code).toBe("VALIDATION_ERROR"); + }); + + test("returns 400 when an unknown dimension is requested", async () => { + const plugin = new AnalyticsPlugin(config); + plugin._setMetricRegistryForTesting({ + revenue: REVENUE_REGISTRATION, + }); + const { router, getHandler } = createMockRouter(); + + plugin.injectRoutes(router); + + const handler = getHandler("POST", "/metric/:key"); + const mockReq = createMockRequest({ + params: { key: "revenue" }, + body: { measures: ["arr"], dimensions: ["nonexistent"] }, + }); + const mockRes = createMockResponse(); + + await handler(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(400); + const errorPayload = (mockRes.json as any).mock.calls[0][0]; + expect(errorPayload.code).toBe("VALIDATION_ERROR"); + }); + + test("returns 400 when an unknown timeGrain is requested", async () => { + const plugin = new AnalyticsPlugin(config); + plugin._setMetricRegistryForTesting({ + revenue: REVENUE_REGISTRATION, + }); + const { router, getHandler } = createMockRouter(); + + plugin.injectRoutes(router); + + const handler = getHandler("POST", "/metric/:key"); + const mockReq = createMockRequest({ + params: { key: "revenue" }, + body: { + measures: ["arr"], + dimensions: ["created_at"], + timeGrain: "year", + }, + }); + const mockRes = createMockResponse(); + + await handler(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(400); + const errorPayload = (mockRes.json as any).mock.calls[0][0]; + expect(errorPayload.code).toBe("VALIDATION_ERROR"); + }); }); diff --git a/packages/appkit/src/plugins/analytics/types.ts b/packages/appkit/src/plugins/analytics/types.ts index 00a55a1dd..0f8946114 100644 --- a/packages/appkit/src/plugins/analytics/types.ts +++ b/packages/appkit/src/plugins/analytics/types.ts @@ -38,27 +38,48 @@ export interface MetricRegistration { /** Lane this metric was registered under. */ lane: MetricLane; /** - * Names of measures known at build time. Empty array means "unknown" — at - * Phase 1 the server falls open in that case (relies on the warehouse to - * reject bad column references). Phase 2 tightens this. + * Names of measures known at build time. Empty array means "unknown" — the + * server falls open in that case and the warehouse rejects bad column + * references. */ knownMeasures: string[]; /** - * Names of dimensions known at build time. Phase 1 does not consume these - * (no GROUP BY yet) but they ride along so Phase 2/3 do not need a second - * loader. + * Names of dimensions known at build time. Phase 2 consumes this for + * dimension validation and `GROUP BY ALL` emission. */ knownDimensions: string[]; + /** + * Map of dimension name → allowed time-grains for that dimension. Only + * populated for time-typed dimensions (those with a `time_grain` attribute + * in the YAML); regular dimensions do not appear in this map. + * + * Empty map means "no time-typed dimensions" — `timeGrain` cannot be set + * on requests for this metric. + */ + knownTimeGrainsByDim: Record; } /** - * Body of POST /api/analytics/metric/:key at Phase 1. + * Body of POST /api/analytics/metric/:key at Phase 2. * - * Phase 2 widens this with `dimensions` + `timeGrain`; Phase 3 adds `filter`. + * Phase 1 shape: `{ measures, format?, limit? }`. Phase 2 widens with + * `dimensions: string[]` and optional `timeGrain`. Phase 3 will add `filter`. */ export interface IAnalyticsMetricRequest { measures: string[]; + /** + * Dimensions to GROUP BY. When non-empty the SQL constructor adds + * `GROUP BY ALL`. When omitted the query is ungrouped (Phase 1 behaviour). + */ + dimensions?: string[]; + /** + * Time-grain truncation applied to every time-typed dimension in + * `dimensions`. The validator rejects this field when no time-typed + * dimension is in `dimensions` (400) and when the value is not in the + * metric view's allowed grain enum (400). + */ + timeGrain?: string; format?: AnalyticsFormat; - /** Optional row cap. Phase 1 only. */ + /** Optional row cap. */ limit?: number; } diff --git a/packages/appkit/src/type-generator/metric-registry.ts b/packages/appkit/src/type-generator/metric-registry.ts index b3f4d2fc2..2a5e8cad1 100644 --- a/packages/appkit/src/type-generator/metric-registry.ts +++ b/packages/appkit/src/type-generator/metric-registry.ts @@ -55,8 +55,9 @@ interface ResolvedMetricEntry { /** * Per-column metadata extracted from DESCRIBE TABLE EXTENDED ... AS JSON. * - * We only need a small subset at Phase 1 (measure names + types). Dimensions - * and YAML metadata land in later phases. + * Phase 1 captured measure flags + types. Phase 2 widens to time-typed + * dimensions: a column is "time-typed" iff its DESCRIBE entry carries a + * non-empty `time_grain` attribute listing the allowed grains for that column. */ export interface MetricColumnMetadata { name: string; @@ -65,6 +66,13 @@ export interface MetricColumnMetadata { isMeasure: boolean; /** Optional column comment / display description (best-effort). */ description?: string; + /** + * Allowed time-grains for this column when present in the YAML's `time_grain` + * attribute. Undefined means the column is not time-typed. An empty array is + * never produced — if the attribute is present but empty we treat the + * column as a regular dimension (matches "explicit only" semantics from the PRD). + */ + timeGrains?: string[]; } /** @@ -324,12 +332,62 @@ export function extractMetricColumns(parsed: unknown): MetricColumnMetadata[] { ? obj.description : undefined; - columns.push({ name, type, isMeasure, description }); + const timeGrains = extractTimeGrains(obj); + + columns.push({ + name, + type, + isMeasure, + description, + ...(timeGrains ? { timeGrains } : {}), + }); } return columns; } +/** + * Pull the allowed time-grain list for a column from the DESCRIBE entry. + * + * Time-grain may live at: + * 1. `time_grain: ["day", "week", "month"]` — the YAML 1.1 canonical form. + * 2. `metadata.time_grain: [...]` — when DESCRIBE wraps it under `metadata`. + * 3. `time_grain: { grains: [...] }` — defensive against future shape drift. + * + * Returns `undefined` for "not a time-typed column" (no attribute present, or + * attribute present but empty/malformed). The caller treats undefined-grains + * dimensions as regular dimensions. + */ +function extractTimeGrains(obj: Record): string[] | undefined { + let raw: unknown = obj.time_grain; + if (raw == null && obj.metadata && typeof obj.metadata === "object") { + raw = (obj.metadata as Record).time_grain; + } + if (raw == null) return undefined; + + if ( + raw && + typeof raw === "object" && + !Array.isArray(raw) && + Array.isArray((raw as Record).grains) + ) { + raw = (raw as Record).grains; + } + + if (!Array.isArray(raw)) return undefined; + const grains: string[] = []; + for (const g of raw) { + if (typeof g === "string" && g.trim().length > 0) { + grains.push(g.toLowerCase().trim()); + } + } + // Empty list → treat as not time-typed (defensive). Phase 2's contract is + // "time_grain populated" → time dimension. + if (grains.length === 0) return undefined; + // Stable order so the generated d.ts and metadata are deterministic. + return [...new Set(grains)].sort(); +} + /** * Map a Databricks SQL type to a TypeScript primitive. * Centralized here (not imported from query-registry) so this module @@ -377,10 +435,13 @@ ${indent}${JSON.stringify(m.name)}: ${tsTypeFor(m.type)}`, const dimensions = schema.dimensions.length > 0 ? schema.dimensions - .map( - (d) => `${indent}/** @sqlType ${d.type} */ -${indent}${JSON.stringify(d.name)}: ${tsTypeFor(d.type)}`, - ) + .map((d) => { + const grainComment = d.timeGrains?.length + ? ` @timeGrain ${d.timeGrains.join("|")}` + : ""; + return `${indent}/** @sqlType ${d.type}${grainComment} */ +${indent}${JSON.stringify(d.name)}: ${tsTypeFor(d.type)}`; + }) .join(";\n") : ""; @@ -404,6 +465,25 @@ ${dimensions}; const dimensionUnion = dimensionKeys.length > 0 ? dimensionKeys.join(" | ") : "never"; + // Union of allowed time-grains across every time-typed dimension. The PRD + // documents the v1 contract: a single top-level `timeGrain` applies to all + // time-typed dims. Therefore the type-level constraint is the union (any of + // the dim-allowed grains is acceptable; per-dim narrowing is a future + // widening to `TimeGrain | Record, TimeGrain>`). + const timeGrainSet = new Set(); + for (const d of schema.dimensions) { + for (const g of d.timeGrains ?? []) { + timeGrainSet.add(g); + } + } + const timeGrainUnion = + timeGrainSet.size > 0 + ? [...timeGrainSet] + .sort() + .map((g) => JSON.stringify(g)) + .join(" | ") + : "never"; + return ` ${JSON.stringify(schema.key)}: { key: ${JSON.stringify(schema.key)}; source: ${JSON.stringify(schema.source)}; @@ -412,6 +492,7 @@ ${dimensions}; dimensions: ${dimensionsBlock}; measureKeys: ${measureUnion}; dimensionKeys: ${dimensionUnion}; + timeGrains: ${timeGrainUnion}; }`; } diff --git a/packages/appkit/src/type-generator/tests/__snapshots__/metric-registry.test.ts.snap b/packages/appkit/src/type-generator/tests/__snapshots__/metric-registry.test.ts.snap index ae98e096f..830cbdffd 100644 --- a/packages/appkit/src/type-generator/tests/__snapshots__/metric-registry.test.ts.snap +++ b/packages/appkit/src/type-generator/tests/__snapshots__/metric-registry.test.ts.snap @@ -1,5 +1,36 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`generateMetricTypeDeclarations — snapshot > emits TimeGrain union for a metric view with time-typed + regular dimensions 1`] = ` +"// Auto-generated by AppKit - DO NOT EDIT +// Generated by 'npx @databricks/appkit generate-types' or Vite plugin during build +import "@databricks/appkit-ui/react"; +declare module "@databricks/appkit-ui/react" { + interface MetricRegistry { + "revenue": { + key: "revenue"; + source: "appkit_demo.public.revenue_metrics_v2"; + lane: "sp"; + measures: { + /** @sqlType DECIMAL(38,2) */ + "arr": number; + }; + dimensions: { + /** @sqlType TIMESTAMP @timeGrain day|month|week */ + "created_at": string; + /** @sqlType STRING */ + "region": string; + /** @sqlType STRING */ + "segment": string; + }; + measureKeys: "arr"; + dimensionKeys: "created_at" | "region" | "segment"; + timeGrains: "day" | "month" | "week"; + }; + } +} +" +`; + exports[`generateMetricTypeDeclarations — snapshot > emits a stable MetricRegistry augmentation for a representative input 1`] = ` "// Auto-generated by AppKit - DO NOT EDIT // Generated by 'npx @databricks/appkit generate-types' or Vite plugin during build @@ -24,6 +55,7 @@ declare module "@databricks/appkit-ui/react" { }; measureKeys: "arr" | "mrr"; dimensionKeys: "region" | "segment"; + timeGrains: never; }; } } diff --git a/packages/appkit/src/type-generator/tests/metric-registry.test.ts b/packages/appkit/src/type-generator/tests/metric-registry.test.ts index 91b5e0d74..a3e7c697b 100644 --- a/packages/appkit/src/type-generator/tests/metric-registry.test.ts +++ b/packages/appkit/src/type-generator/tests/metric-registry.test.ts @@ -207,6 +207,80 @@ describe("extractMetricColumns", () => { test("returns empty array on unrecognized shape", () => { expect(extractMetricColumns({ unrelated: true })).toEqual([]); }); + + // ── Phase 2: time-typed dimensions ──────────────────────────────────── + test("captures time_grain attribute on a time-typed dimension", () => { + const cols = extractMetricColumns({ + columns: [ + { + name: "created_at", + type: "DATE", + is_measure: false, + time_grain: ["day", "week", "month"], + }, + { name: "region", type: "STRING", is_measure: false }, + ], + }); + expect(cols).toHaveLength(2); + expect(cols[0]).toMatchObject({ + name: "created_at", + type: "DATE", + isMeasure: false, + timeGrains: ["day", "month", "week"], // sorted, deduped + }); + // Non-time dim has no timeGrains key. + expect(cols[1].timeGrains).toBeUndefined(); + }); + + test("normalizes time_grain values to lowercase + sorted + deduped", () => { + const cols = extractMetricColumns({ + columns: [ + { + name: "ts", + type: "TIMESTAMP", + is_measure: false, + time_grain: ["MONTH", "day", "Day", "week"], + }, + ], + }); + expect(cols[0].timeGrains).toEqual(["day", "month", "week"]); + }); + + test("falls back to metadata.time_grain (DESCRIBE wraps it under metadata)", () => { + const cols = extractMetricColumns({ + columns: [ + { + name: "ts", + type: "TIMESTAMP", + metadata: { is_measure: false, time_grain: ["day"] }, + }, + ], + }); + expect(cols[0].timeGrains).toEqual(["day"]); + }); + + test("treats empty time_grain attribute as not time-typed", () => { + const cols = extractMetricColumns({ + columns: [ + { name: "ts", type: "TIMESTAMP", is_measure: false, time_grain: [] }, + ], + }); + expect(cols[0].timeGrains).toBeUndefined(); + }); + + test("ignores non-string time_grain entries", () => { + const cols = extractMetricColumns({ + columns: [ + { + name: "ts", + type: "TIMESTAMP", + is_measure: false, + time_grain: ["day", null, 42, "week"], + }, + ], + }); + expect(cols[0].timeGrains).toEqual(["day", "week"]); + }); }); describe("syncMetrics", () => { @@ -282,4 +356,74 @@ describe("generateMetricTypeDeclarations — snapshot", () => { const output = generateMetricTypeDeclarations([]); expect(output).toMatchSnapshot(); }); + + // ── Phase 2: time-typed dim + multiple non-time dims fixture ───────── + test("emits TimeGrain union for a metric view with time-typed + regular dimensions", async () => { + const resolution = resolveMetricConfig({ + sp: { + revenue: { source: "appkit_demo.public.revenue_metrics_v2" }, + }, + }); + + const fetcher = async () => + mockDescribeResponse({ + columns: [ + { + name: "arr", + type: "DECIMAL(38,2)", + is_measure: true, + comment: "Annual recurring revenue", + }, + { + name: "created_at", + type: "TIMESTAMP", + is_measure: false, + time_grain: ["day", "week", "month"], + }, + { name: "region", type: "STRING", is_measure: false }, + { name: "segment", type: "STRING", is_measure: false }, + ], + }); + + const schemas = await syncMetrics(resolution, fetcher); + const output = generateMetricTypeDeclarations(schemas); + expect(output).toMatchSnapshot(); + + // Sanity assertions in addition to the snapshot, so future drift surfaces + // even when snapshots are blindly updated. + expect(output).toContain('timeGrains: "day" | "month" | "week"'); + expect(output).toContain("@timeGrain day|month|week"); + expect(output).toContain('"created_at": string'); + expect(output).toContain('"region": string'); + }); +}); + +// ── Phase 2: syncMetrics propagates timeGrains end-to-end ──────────────── +describe("syncMetrics — time-typed dimension propagation", () => { + test("propagates the time_grain attribute onto the resulting MetricSchema", async () => { + const resolution = resolveMetricConfig({ + sp: { revenue: { source: "demo.public.revenue" } }, + }); + + const fetcher = async () => + mockDescribeResponse({ + columns: [ + { name: "arr", type: "DECIMAL", is_measure: true }, + { + name: "ts", + type: "TIMESTAMP", + is_measure: false, + time_grain: ["day", "month"], + }, + { name: "region", type: "STRING", is_measure: false }, + ], + }); + + const schemas = await syncMetrics(resolution, fetcher); + expect(schemas[0].dimensions).toHaveLength(2); + const tsDim = schemas[0].dimensions.find((d) => d.name === "ts"); + expect(tsDim?.timeGrains).toEqual(["day", "month"]); + const regionDim = schemas[0].dimensions.find((d) => d.name === "region"); + expect(regionDim?.timeGrains).toBeUndefined(); + }); }); From 217847657e99b84e67c15237e12f746f5c1f5ad0 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Wed, 29 Apr 2026 23:53:10 +0200 Subject: [PATCH 03/34] =?UTF-8?q?feat(appkit):=20metric=20view=20source=20?= =?UTF-8?q?=E2=80=94=20Phase=203=20structured=20filter=20spec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the recursive Filter type to the metric-view request body, validator, and SQL constructor. 12 v1 operators, AND/OR composition, member restricted to dimensions, all values bound via the existing Statement Execution parameter path. No string concatenation of user input. Filter = Predicate | { and: Filter[] } | { or: Filter[] } Predicate = { member: DimensionKey; operator; values? } Operators: equals, notEquals, in, notIn, gt, gte, lt, lte, contains, notContains, set, notSet. Member must resolve in DimensionKey; op⇄type compatibility (range ops on numeric/date only, string ops on string only, others any-type) enforced when knownDimensionTypes is populated. Cardinality rules per op (single / list / null). Recursion depth cap = 8. SQL: recursive renderFilter emits parameterized fragments with named binds (:f_). AND/OR groups parenthesized. WHERE clause omitted when filter is empty. Cache key composer extends with sorted filter children inside each AND/OR group (sort-before-hash for the filter sub-tree; full args sort lands in Phase 4). Tests: +58 (1936 total). 12 op cases, AND/OR composition, depth-cap, value- not-in-SQL parameterization assertions, validator rejections, type-level narrowing for Predicate.member and Filter recursion, hook payload assembly preserves recursive structure. Generated-file diffs in registry/types.generated.ts and shared/schemas/ *.generated.ts are benign Biome formatting picked up by check:fix during the build cycle — no logic change. Per prd/analytics-metric-view-source and tasks/.../Phase 3. xavier loop iteration 4. Co-authored-by: Isaac Signed-off-by: Atila Fassina --- .../__tests__/use-metric-view-types.test.ts | 86 ++ .../hooks/__tests__/use-metric-view.test.ts | 77 ++ packages/appkit-ui/src/react/hooks/index.ts | 3 + packages/appkit-ui/src/react/hooks/types.ts | 87 ++ .../src/react/hooks/use-metric-view.ts | 6 + .../appkit/src/plugins/analytics/analytics.ts | 5 +- .../appkit/src/plugins/analytics/metric.ts | 729 ++++++++++++++++- .../plugins/analytics/tests/metric.test.ts | 749 +++++++++++++++++- .../appkit/src/plugins/analytics/types.ts | 81 +- 9 files changed, 1790 insertions(+), 33 deletions(-) diff --git a/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view-types.test.ts b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view-types.test.ts index 84455d66b..edd061cec 100644 --- a/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view-types.test.ts +++ b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view-types.test.ts @@ -1,7 +1,10 @@ import { describe, expectTypeOf, test } from "vitest"; import type { DimensionKey, + Filter, MeasureKey, + MetricFilterOperator, + Predicate, TimeGrain, UseMetricViewArgs, UseMetricViewRow, @@ -135,3 +138,86 @@ describe("UseMetricViewRow — row narrowing via Pick", () => { expectTypeOf().toEqualTypeOf<{ segment: string }>(); }); }); + +describe("Filter / Predicate — recursive shape and registry narrowing", () => { + test("Predicate.member narrows to DimensionKey", () => { + type RevenueMember = Predicate<"revenue">["member"]; + expectTypeOf().toEqualTypeOf< + "region" | "segment" | "created_at" + >(); + }); + + test("Predicate.operator narrows to MetricFilterOperator (12 v1 ops)", () => { + type Op = Predicate<"revenue">["operator"]; + expectTypeOf().toEqualTypeOf(); + }); + + test("MetricFilterOperator union has exactly 12 members", () => { + type Op = MetricFilterOperator; + // exactness guard: assignability both ways + expectTypeOf().toEqualTypeOf< + | "equals" + | "notEquals" + | "in" + | "notIn" + | "gt" + | "gte" + | "lt" + | "lte" + | "contains" + | "notContains" + | "set" + | "notSet" + >(); + }); + + test("Filter accepts a leaf Predicate", () => { + const leaf: Filter<"revenue"> = { + member: "region", + operator: "equals", + values: ["EMEA"], + }; + expectTypeOf(leaf).toMatchTypeOf>(); + }); + + test("Filter accepts an { and: Filter[] } group (recursive)", () => { + const grouped: Filter<"revenue"> = { + and: [ + { member: "region", operator: "equals", values: ["EMEA"] }, + { member: "segment", operator: "equals", values: ["Enterprise"] }, + ], + }; + expectTypeOf(grouped).toMatchTypeOf>(); + }); + + test("Filter accepts an { or: Filter[] } group with nested AND (recursive)", () => { + const nested: Filter<"revenue"> = { + or: [ + { + and: [ + { member: "region", operator: "equals", values: ["EMEA"] }, + { member: "segment", operator: "equals", values: ["Enterprise"] }, + ], + }, + { member: "region", operator: "equals", values: ["APAC"] }, + ], + }; + expectTypeOf(nested).toMatchTypeOf>(); + }); + + test("UseMetricViewArgs accepts an optional filter narrowing to DimensionKey", () => { + type Args = UseMetricViewArgs< + "revenue", + readonly ["arr"], + readonly ["region"] + >; + expectTypeOf().toEqualTypeOf< + Filter<"revenue"> | undefined + >(); + }); + + test("Predicate.member is `never` when the registry declares no dimensions", () => { + type Member = Predicate<"flat_metric">["member"]; + expectTypeOf().toEqualTypeOf(); + }); +}); diff --git a/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts index d1c2e670a..fb2cce7e3 100644 --- a/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts +++ b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts @@ -148,6 +148,83 @@ describe("useMetricView", () => { }); }); + // ── Phase 3: filter payload assembly ───────────────────────────────────── + test("includes a leaf Predicate filter in the payload", () => { + renderHook(() => + useMetricView("revenue", { + measures: ["arr"], + dimensions: ["region"], + filter: { + member: "region", + operator: "equals", + values: ["EMEA"], + }, + } as any), + ); + + const payload = JSON.parse( + (mockConnectSSE.mock.calls[0][0] as any).payload, + ); + expect(payload.filter).toEqual({ + member: "region", + operator: "equals", + values: ["EMEA"], + }); + }); + + test("preserves recursive { and: [...] } filter structure verbatim", () => { + const filter = { + and: [ + { member: "region", operator: "in", values: ["EMEA", "APAC"] }, + { member: "segment", operator: "equals", values: ["Enterprise"] }, + ], + }; + renderHook(() => + useMetricView("revenue", { + measures: ["arr"], + filter, + } as any), + ); + + const payload = JSON.parse( + (mockConnectSSE.mock.calls[0][0] as any).payload, + ); + expect(payload.filter).toEqual(filter); + }); + + test("preserves deeply-nested OR-of-AND structure", () => { + const filter = { + or: [ + { + and: [ + { member: "region", operator: "equals", values: ["EMEA"] }, + { member: "segment", operator: "equals", values: ["Enterprise"] }, + ], + }, + { member: "region", operator: "equals", values: ["APAC"] }, + ], + }; + renderHook(() => + useMetricView("revenue", { + measures: ["arr"], + filter, + } as any), + ); + + const payload = JSON.parse( + (mockConnectSSE.mock.calls[0][0] as any).payload, + ); + expect(payload.filter).toEqual(filter); + }); + + test("omits filter from the payload when not provided", () => { + renderHook(() => useMetricView("revenue", { measures: ["arr"] })); + const payload = JSON.parse( + (mockConnectSSE.mock.calls[0][0] as any).payload, + ); + expect(payload.filter).toBeUndefined(); + }); + test("populates data on a result event", async () => { const { result } = renderHook(() => useMetricView("revenue", { measures: ["arr"] }), diff --git a/packages/appkit-ui/src/react/hooks/index.ts b/packages/appkit-ui/src/react/hooks/index.ts index 1572de498..32998a091 100644 --- a/packages/appkit-ui/src/react/hooks/index.ts +++ b/packages/appkit-ui/src/react/hooks/index.ts @@ -1,16 +1,19 @@ export type { AnalyticsFormat, DimensionKey, + Filter, InferResultByFormat, InferRowType, InferServingChunk, InferServingRequest, InferServingResponse, MeasureKey, + MetricFilterOperator, MetricKey, MetricRegistry, MetricRow, PluginRegistry, + Predicate, QueryRegistry, ServingAlias, ServingEndpointRegistry, diff --git a/packages/appkit-ui/src/react/hooks/types.ts b/packages/appkit-ui/src/react/hooks/types.ts index 009f36410..4a47dd2d5 100644 --- a/packages/appkit-ui/src/react/hooks/types.ts +++ b/packages/appkit-ui/src/react/hooks/types.ts @@ -231,6 +231,80 @@ type MetricDimensionMap = K extends AugmentedRegistry /** Full result row type for a registered metric (measures + dimensions). */ export type MetricRow = MetricMeasureMap & MetricDimensionMap; +// ============================================================================ +// Filter Specification (Phase 3 — recursive AND/OR with 12 v1 operators) +// ============================================================================ + +/** + * The v1 filter operator vocabulary. Twelve operators, exactly: + * + * - Equality: `equals`, `notEquals` + * - Set membership: `in`, `notIn` + * - Range: `gt`, `gte`, `lt`, `lte` + * - String search: `contains`, `notContains` + * - NULL checks: `set`, `notSet` + * + * Operator-vs-type rules (enforced server-side): + * - Range ops (`gt`, `gte`, `lt`, `lte`) require a numeric / date-typed dim. + * - String ops (`contains`, `notContains`) require a string-typed dim. + * - The remaining six accept any dimension type. + * + * Cardinality rules (enforced server-side): + * - Single-value ops (`equals`, `notEquals`, `gt`, `gte`, `lt`, `lte`, + * `contains`, `notContains`) require exactly one value. + * - List ops (`in`, `notIn`) require at least one value. + * - NULL ops (`set`, `notSet`) reject `values` entirely. + */ +export type MetricFilterOperator = + | "equals" + | "notEquals" + | "in" + | "notIn" + | "gt" + | "gte" + | "lt" + | "lte" + | "contains" + | "notContains" + | "set" + | "notSet"; + +/** + * A single filter predicate — leaf node of the recursive {@link Filter} tree. + * + * `member` narrows to the union of dimension names declared on the metric + * view (HAVING — filtering on measures — is reserved for v1.5). + * + * `values` is optional; the validator rejects requests where `values` is + * present for `set`/`notSet` and absent for every other operator. + */ +export interface Predicate { + member: DimensionKey; + operator: MetricFilterOperator; + values?: ReadonlyArray; +} + +/** + * The recursive filter type for metric views. + * + * A `Filter` is one of: + * - a leaf {@link Predicate} + * - an `{ and: Filter[] }` group — every child predicate must match + * - an `{ or: Filter[] }` group — at least one child predicate must match + * + * The shape supports nesting from v1; flat consumers can pass an array of + * predicates either via `{ and: [...] }` (explicit AND) or — since the wire + * shape carries the full union — by composing a single-level `{ and }` + * wrapper on the client. + * + * Server-side, recursion is depth-capped so a malformed or hostile payload + * cannot stack-overflow the validator. + */ +export type Filter = + | Predicate + | { and: ReadonlyArray> } + | { or: ReadonlyArray> }; + /** * Phase 2 args: measures + dimensions + optional time grain. * @@ -272,6 +346,19 @@ export interface UseMetricViewArgs< * `dimensions` is a server-side 400. */ timeGrain?: TimeGrain; + /** + * Optional structured filter — recursive AND/OR composition of predicates. + * + * `member` narrows to the metric's declared dimension names (the IDE + * catches typos at the call site). `operator` narrows to the 12 v1 + * operators. All `values` are bound as parameters server-side; nothing + * from the request body flows into the rendered SQL string. + * + * The filter shape is recursive from day one — flat callers can wrap a + * predicate list in `{ and: [...] }`; nested callers can mix `and`/`or` + * groups freely. The server enforces a depth cap to prevent stack abuse. + */ + filter?: Filter; /** Optional row cap. */ limit?: number; } diff --git a/packages/appkit-ui/src/react/hooks/use-metric-view.ts b/packages/appkit-ui/src/react/hooks/use-metric-view.ts index a67d0c509..de19ae666 100644 --- a/packages/appkit-ui/src/react/hooks/use-metric-view.ts +++ b/packages/appkit-ui/src/react/hooks/use-metric-view.ts @@ -74,6 +74,11 @@ export function useMetricView< if (typeof args.timeGrain === "string" && args.timeGrain.length > 0) { body.timeGrain = args.timeGrain; } + if (args.filter !== undefined) { + // Filter is a recursive AND/OR/Predicate tree; preserve structure + // verbatim — the server validates and translates it into SQL. + body.filter = args.filter; + } if (typeof args.limit === "number") { body.limit = args.limit; } @@ -93,6 +98,7 @@ export function useMetricView< args.measures, args.dimensions, args.timeGrain, + args.filter, args.limit, format, maxParametersSize, diff --git a/packages/appkit/src/plugins/analytics/analytics.ts b/packages/appkit/src/plugins/analytics/analytics.ts index 25d805798..43f6aea40 100644 --- a/packages/appkit/src/plugins/analytics/analytics.ts +++ b/packages/appkit/src/plugins/analytics/analytics.ts @@ -328,6 +328,7 @@ export class AnalyticsPlugin extends Plugin { measures: request.measures, dimensions: request.dimensions, timeGrain: request.timeGrain, + filter: request.filter, format, executorKey, limit: request.limit, @@ -348,10 +349,10 @@ export class AnalyticsPlugin extends Plugin { await executor.executeStream( res, async (signal) => { - const { statement } = buildMetricSql(registration, request); + const { statement, parameters } = buildMetricSql(registration, request); const result = await executor.query( statement, - undefined, + Object.keys(parameters).length > 0 ? parameters : undefined, queryParameters.formatParameters, signal, ); diff --git a/packages/appkit/src/plugins/analytics/metric.ts b/packages/appkit/src/plugins/analytics/metric.ts index 1db959101..2fced8c8d 100644 --- a/packages/appkit/src/plugins/analytics/metric.ts +++ b/packages/appkit/src/plugins/analytics/metric.ts @@ -1,16 +1,88 @@ import fs from "node:fs/promises"; import path from "node:path"; +import { type SQLTypeMarker, sql as sqlHelpers } from "shared"; import { z } from "zod"; import { ValidationError } from "../../errors"; import { createLogger } from "../../logging/logger"; import type { IAnalyticsMetricRequest, + MetricDimensionTypeClass, + MetricFilter, + MetricFilterOperatorName, MetricLane, + MetricPredicate, MetricRegistration, } from "./types"; const logger = createLogger("analytics:metric"); +/** + * The exact twelve filter operators allowed at v1. The runtime tuple is the + * server-side source of truth; the client-side type union + * `MetricFilterOperator` mirrors these names statically. + */ +const METRIC_FILTER_OPERATORS = [ + "equals", + "notEquals", + "in", + "notIn", + "gt", + "gte", + "lt", + "lte", + "contains", + "notContains", + "set", + "notSet", +] as const satisfies readonly MetricFilterOperatorName[]; + +/** + * Maximum AND/OR nesting depth. The PRD documents 8 as a sensible cap — + * enough for any real BI filter UI, low enough that a hostile or malformed + * payload cannot stack-overflow the recursive validator or translator. + * + * The depth count is the number of nested `{ and }` / `{ or }` wrappers + * encountered while descending — leaf predicates do not count toward depth. + */ +const METRIC_FILTER_MAX_DEPTH = 8; + +/** + * Range ops — require numeric or date-typed dimensions. The remaining ops + * split into: + * - any-type: equals, notEquals, in, notIn, set, notSet + * - string-only: contains, notContains + */ +const RANGE_OPERATORS = new Set([ + "gt", + "gte", + "lt", + "lte", +]); + +/** String ops — require string-typed dimensions. */ +const STRING_OPERATORS = new Set([ + "contains", + "notContains", +]); + +/** Operators that require exactly one value. */ +const SINGLE_VALUE_OPERATORS = new Set([ + "equals", + "notEquals", + "gt", + "gte", + "lt", + "lte", + "contains", + "notContains", +]); + +/** Operators that require at least one value. */ +const LIST_VALUE_OPERATORS = new Set(["in", "notIn"]); + +/** Operators that reject `values` entirely. */ +const NULL_OPERATORS = new Set(["set", "notSet"]); + /** * Default queries directory. Mirrors `AppManager.queriesDir` so dev mode and * production share a single source of truth. @@ -157,7 +229,7 @@ export async function loadMetricRegistry( * metadata available) any non-empty string is accepted and validation defers * to the warehouse. * - * Phase 2 body shape: `{ measures, dimensions?, timeGrain?, format?, limit? }`. + * Phase 3 body shape: `{ measures, dimensions?, timeGrain?, filter?, format?, limit? }`. * * Validation matrix: * - `measures` — must be a non-empty array; constrained to `knownMeasures` @@ -166,6 +238,11 @@ export async function loadMetricRegistry( * - `timeGrain` — optional string; constrained to the union of grains * declared across all time-typed dimensions; rejected unless the * `dimensions` array contains at least one time-typed dimension. + * - `filter` — optional recursive AND/OR tree of predicates; `member` + * constrained to `knownDimensions`; `operator` constrained to the v1 + * twelve; op⇄type compatibility enforced when dimension types are + * available; values cardinality enforced per operator; AND/OR depth + * capped at {@link METRIC_FILTER_MAX_DEPTH}. */ export function makeMetricRequestSchema( registration: MetricRegistration, @@ -216,6 +293,42 @@ export function makeMetricRequestSchema( }) : baseTimeGrainSchema; + // ── Filter sub-schema (Phase 3) ────────────────────────────────────────── + // + // The filter shape is recursive (`Predicate | { and: [...] } | { or: [...] }`). + // Zod's recursive support uses `z.lazy(() => ...)` — the depth cap and the + // op⇄type compatibility check live in a `superRefine` on the parent (so we + // can walk the tree once with full context). + const filterPredicateSchema: z.ZodType = z + .object({ + member: z + .string() + .min(1, { message: "filter predicate 'member' cannot be empty" }), + operator: z.string().min(1, { + message: "filter predicate 'operator' cannot be empty", + }) as z.ZodType, + values: z.array(z.union([z.string(), z.number()])).optional(), + }) + .strict(); + + const filterSchema: z.ZodType = z.lazy(() => + z.union([ + filterPredicateSchema, + z + .object({ + and: z.array(filterSchema), + }) + .strict(), + z + .object({ + or: z.array(filterSchema), + }) + .strict(), + ]), + ); + + const knownDimensionTypes = registration.knownDimensionTypes ?? {}; + const baseObject = z .object({ measures: z @@ -223,6 +336,7 @@ export function makeMetricRequestSchema( .min(1, { message: "measures must contain at least one entry" }), dimensions: z.array(dimensionItemSchema).optional(), timeGrain: timeGrainSchema.optional(), + filter: filterSchema.optional(), format: z.enum(["JSON", "ARROW"]).optional(), limit: z .number() @@ -232,24 +346,236 @@ export function makeMetricRequestSchema( }) .strict(); - // Cross-field rule: timeGrain is meaningless without a time-typed dimension - // in the dimensions list. Failing fast here keeps the SQL constructor - // honest (no `date_trunc(, )` without a real column to truncate). + // Cross-field rules: + // 1. timeGrain is meaningless without a time-typed dimension in the + // dimensions list. Failing fast here keeps the SQL constructor honest + // (no `date_trunc(, )` without a real column to truncate). + // 2. The recursive `filter` tree is depth-walked once: every predicate's + // member must be a registered dimension; every operator must be one of + // the twelve; op⇄type compatibility is enforced when dimension types + // are available; values cardinality is enforced per operator; AND/OR + // nesting is capped at METRIC_FILTER_MAX_DEPTH. return baseObject.superRefine((value, ctx) => { - if (value.timeGrain == null) return; - const dims = value.dimensions ?? []; - const hasTimeDim = dims.some( - (d) => Array.isArray(grainsByDim[d]) && grainsByDim[d].length > 0, - ); - if (!hasTimeDim) { + if (value.timeGrain != null) { + const dims = value.dimensions ?? []; + const hasTimeDim = dims.some( + (d) => Array.isArray(grainsByDim[d]) && grainsByDim[d].length > 0, + ); + if (!hasTimeDim) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["timeGrain"], + message: + "timeGrain specified but no time-typed dimension is included in 'dimensions'", + }); + } + } + + if (value.filter != null) { + validateFilterTree(value.filter, ctx, ["filter"], 0, { + knownDimensions, + knownDimensionTypes, + }); + } + }) as z.ZodType; +} + +/** + * Recursive zod-time validator for the filter tree. + * + * Pushes structured issues into the zod refinement context with stable paths + * (`filter.and.0.or.2.member`, etc.) so the canonical 400 error shape carries + * actionable diagnostics. Keeps three concerns in one descent: + * + * 1. Member is a registered dimension (when registry has metadata). + * 2. Operator is one of the twelve; values cardinality matches. + * 3. Op⇄type compatibility (string ops on string-typed dims, range ops on + * numeric/date-typed dims, equality/set/null ops on any type). + * 4. Depth cap (AND/OR nesting limit). + * + * Returns void; issues are accumulated on `ctx`. The caller's + * `safeParse(...).success` flips false when any issue is added. + */ +function validateFilterTree( + node: MetricFilter, + ctx: z.RefinementCtx, + path: Array, + depth: number, + registry: { + knownDimensions: string[]; + knownDimensionTypes: Record; + }, +): void { + if (node === null || typeof node !== "object") { + // The base schema rejects this case earlier via the union, but be + // defensive in case a future refactor leaves the door ajar. + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path, + message: "filter node must be a Predicate or { and } / { or } group", + }); + return; + } + + if ("and" in node || "or" in node) { + if (depth + 1 > METRIC_FILTER_MAX_DEPTH) { ctx.addIssue({ code: z.ZodIssueCode.custom, - path: ["timeGrain"], - message: - "timeGrain specified but no time-typed dimension is included in 'dimensions'", + path, + message: `filter AND/OR nesting exceeds the maximum depth of ${METRIC_FILTER_MAX_DEPTH}`, }); + return; } - }) as z.ZodType; + + const groupKey = "and" in node ? "and" : "or"; + const children = ( + node as { and?: ReadonlyArray } & { + or?: ReadonlyArray; + } + )[groupKey]; + + if (!Array.isArray(children)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [...path, groupKey], + message: `filter ${groupKey} group must be an array of predicates or nested groups`, + }); + return; + } + + children.forEach((child, idx) => { + validateFilterTree( + child, + ctx, + [...path, groupKey, idx], + depth + 1, + registry, + ); + }); + return; + } + + // Leaf predicate. The base schema already enforced shape; here we layer in + // the registry-aware constraints. + const predicate = node as MetricPredicate; + + if ( + registry.knownDimensions.length > 0 && + !registry.knownDimensions.includes(predicate.member) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [...path, "member"], + message: `filter member "${predicate.member}" is not a declared dimension (allowed: ${registry.knownDimensions.join(", ")})`, + }); + } + + if ( + !METRIC_FILTER_OPERATORS.includes( + predicate.operator as MetricFilterOperatorName, + ) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [...path, "operator"], + message: `filter operator "${predicate.operator}" is not one of: ${METRIC_FILTER_OPERATORS.join(", ")}`, + }); + // No further checks meaningful when the operator is unknown. + return; + } + + const op = predicate.operator; + const values = predicate.values; + const valuesLen = values?.length ?? 0; + + if (NULL_OPERATORS.has(op)) { + if (values != null && valuesLen > 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [...path, "values"], + message: `filter operator "${op}" must not carry values`, + }); + } + } else if (SINGLE_VALUE_OPERATORS.has(op)) { + if (valuesLen !== 1) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [...path, "values"], + message: `filter operator "${op}" requires exactly one value (got ${valuesLen})`, + }); + } + } else if (LIST_VALUE_OPERATORS.has(op)) { + if (valuesLen < 1) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [...path, "values"], + message: `filter operator "${op}" requires at least one value`, + }); + } + } + + // Op⇄type compatibility — only enforced when we have a registered type. + // Falls open (no error) when the registry didn't supply a type for the dim. + const declaredType = registry.knownDimensionTypes[predicate.member]; + if (declaredType) { + const cls = classifyDimensionType(declaredType); + if (RANGE_OPERATORS.has(op) && cls === "string") { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [...path, "operator"], + message: `filter operator "${op}" is incompatible with string-typed dimension "${predicate.member}"`, + }); + } + if (STRING_OPERATORS.has(op) && cls !== "string" && cls !== "unknown") { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [...path, "operator"], + message: `filter operator "${op}" is incompatible with non-string dimension "${predicate.member}" (type ${declaredType})`, + }); + } + } +} + +/** + * Classify a Databricks SQL type string into a coarse compatibility class. + * + * The classification is conservative: `STRING` and adjacent text types map to + * `string`; numeric, integral, and float types map to `numeric`; `DATE` and + * `TIMESTAMP` map to `date`; everything else maps to `unknown`. Accepting the + * fallback as `unknown` lets the validator stay deterministic when the + * registry has no type metadata for the dim. + */ +function classifyDimensionType(sqlType: string): MetricDimensionTypeClass { + const normalized = sqlType + .toUpperCase() + .replace(/\(.*\)$/, "") + .replace(/<.*>$/, "") + .split(" ")[0]; + + switch (normalized) { + case "STRING": + case "VARCHAR": + case "CHAR": + case "TEXT": + return "string"; + case "TINYINT": + case "SMALLINT": + case "INT": + case "INTEGER": + case "BIGINT": + case "FLOAT": + case "DOUBLE": + case "DECIMAL": + case "NUMERIC": + return "numeric"; + case "DATE": + case "TIMESTAMP": + case "TIMESTAMP_NTZ": + case "TIMESTAMP_LTZ": + return "date"; + default: + return "unknown"; + } } /** @@ -335,20 +661,21 @@ const DIMENSION_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/; const TIME_GRAIN_PATTERN = /^[a-z][a-z_]*$/; /** - * Construct the Phase 2 metric SQL. + * Construct the Phase 3 metric SQL. * * Shape: * * SELECT MEASURE(m), date_trunc('', ) AS , * FROM + * [WHERE ] * [GROUP BY ALL] * [LIMIT n] * * Notes: - * - All column references (measures, dimensions) are validated against the - * registry's `knownMeasures` / `knownDimensions` and against the conservative - * identifier pattern. No user-supplied string flows into the SQL string - * without passing both gates. + * - All column references (measures, dimensions, filter members) are + * validated against the registry and against the conservative identifier + * pattern. No user-supplied string flows into the SQL string without + * passing both gates. * - `date_trunc('', col) AS col` is emitted for every time-typed * dimension when `timeGrain` is set. The grain literal is single-quoted in * the SQL — we cannot use a bind variable for `date_trunc`'s first @@ -357,11 +684,21 @@ const TIME_GRAIN_PATTERN = /^[a-z][a-z_]*$/; * requires GROUP BY when MEASURE() is mixed with non-aggregated columns; * `GROUP BY ALL` is the documented form that works without re-listing each * dimension. + * - `WHERE` clause is rendered from the recursive filter tree. Every value + * flows through Statement Execution's named bind-var path (`:f_`); + * no value is ever interpolated as a literal. Member identifiers come + * from the validated registry, not the request body. + * + * Returns `{ statement, parameters }` where `parameters` is the named + * bind-var dictionary the analytics plugin's `query()` method consumes. */ export function buildMetricSql( registration: MetricRegistration, request: IAnalyticsMetricRequest, -): { statement: string; parameters: never[] } { +): { + statement: string; + parameters: Record; +} { assertSafeFqn(registration.source); if (request.measures.length === 0) { @@ -440,8 +777,300 @@ export function buildMetricSql( ? ` LIMIT ${Math.floor(request.limit)}` : ""; - const statement = `SELECT ${selectList} FROM ${registration.source}${groupByClause}${limitClause}`; - return { statement, parameters: [] }; + // Filter translation. Every value is bound through `:f_` named params; + // every column identifier is gated by the registry-membership check above + // (recursively, via `renderFilter`). Empty filter or no filter → no WHERE. + const parameters: Record = {}; + let whereClause = ""; + if (request.filter !== undefined) { + const fragment = renderFilter(request.filter, registration, parameters, { + counter: 0, + depth: 0, + }); + if (fragment !== null && fragment.length > 0) { + whereClause = ` WHERE ${fragment}`; + } + } + + const statement = `SELECT ${selectList} FROM ${registration.source}${whereClause}${groupByClause}${limitClause}`; + return { statement, parameters }; +} + +/** + * Mutable counter / depth threaded through {@link renderFilter}. Fresh per + * `buildMetricSql` call, so two requests never share bind-var indexes. + */ +interface FilterRenderState { + counter: number; + depth: number; +} + +/** + * Recursively render a filter tree into a SQL fragment, pushing bind values + * into `params` keyed by `:f_` names. + * + * Returns `null` for an empty group (no WHERE clause needed). The caller's + * `buildMetricSql` only emits `WHERE` when this returns a non-null, + * non-empty fragment. Empty `and: []` and `or: []` groups collapse to null — + * matching SQL's vacuous-truth semantics for AND, and the validator-permitted + * "no predicates" shape. + * + * Defense-in-depth: even though the request body's filter has already been + * validated by the zod schema, every member name is re-checked against the + * registry here. If validation is ever bypassed, the SQL constructor still + * refuses to interpolate an unknown identifier. + */ +function renderFilter( + node: MetricFilter, + registration: MetricRegistration, + params: Record, + state: FilterRenderState, +): string | null { + if (node === null || typeof node !== "object") { + throw new Error( + "Refusing to build SQL: filter node must be an object Predicate or { and } / { or } group.", + ); + } + + if ("and" in node || "or" in node) { + const groupKey = "and" in node ? "and" : "or"; + if (state.depth + 1 > METRIC_FILTER_MAX_DEPTH) { + throw new Error( + `Refusing to build SQL: filter AND/OR nesting exceeds the maximum depth of ${METRIC_FILTER_MAX_DEPTH}.`, + ); + } + + const children = ( + node as { and?: ReadonlyArray } & { + or?: ReadonlyArray; + } + )[groupKey]; + + if (!Array.isArray(children) || children.length === 0) { + // Empty group → no constraint; an empty AND is vacuously true and an + // empty OR is the validator's "do not contribute" shape. + return null; + } + + // Sort-before-hash discipline (Phase 3 incremental). Within a group, + // predicate leaves are stable-sorted by (member, operator) before + // contributing to the rendered fragment, so semantically equivalent calls + // produce the same SQL string and (downstream) the same cache key. + const sortedChildren = sortFilterChildren(children); + + const fragments: string[] = []; + const childState: FilterRenderState = { + counter: state.counter, + depth: state.depth + 1, + }; + for (const child of sortedChildren) { + const rendered = renderFilter(child, registration, params, childState); + if (rendered != null && rendered.length > 0) { + fragments.push(rendered); + } + } + state.counter = childState.counter; + + if (fragments.length === 0) return null; + if (fragments.length === 1) return fragments[0]; + const joiner = groupKey === "and" ? " AND " : " OR "; + return `(${fragments.join(joiner)})`; + } + + // Leaf predicate — validate against the registry one more time, then render. + const predicate = node as MetricPredicate; + + if (!DIMENSION_NAME_PATTERN.test(predicate.member)) { + throw new Error( + `Refusing to build SQL: filter member "${predicate.member}" is not a valid identifier.`, + ); + } + if ( + registration.knownDimensions.length > 0 && + !registration.knownDimensions.includes(predicate.member) + ) { + throw new Error( + `Refusing to build SQL: unknown filter member "${predicate.member}" for metric "${registration.key}".`, + ); + } + if ( + !METRIC_FILTER_OPERATORS.includes( + predicate.operator as MetricFilterOperatorName, + ) + ) { + throw new Error( + `Refusing to build SQL: unknown filter operator "${predicate.operator}".`, + ); + } + + return renderPredicate(predicate, params, state); +} + +/** + * Stable-sort filter children inside an AND/OR group by `(member, operator)`. + * + * Predicates carry both fields and sort by their pair; nested groups sort + * after predicates and stay in their original relative order (a nested group + * is opaque from the outside — we cannot collapse it to a single key). This + * is the sort-before-hash invariant applied at the SQL-fragment level so + * downstream cache keys collapse semantically equivalent calls. + */ +function sortFilterChildren( + children: ReadonlyArray, +): MetricFilter[] { + const indexed = children.map((child, idx) => { + let key: string; + let isPredicate: boolean; + if ( + child !== null && + typeof child === "object" && + !("and" in child) && + !("or" in child) + ) { + const p = child as MetricPredicate; + key = `${p.member}${p.operator}`; + isPredicate = true; + } else { + // Nested groups don't have a single (member, operator) — keep their + // original index so multiple nested groups within the same parent + // remain stable relative to each other. + key = ""; + isPredicate = false; + } + return { child, idx, key, isPredicate }; + }); + + indexed.sort((a, b) => { + if (a.isPredicate && !b.isPredicate) return -1; + if (!a.isPredicate && b.isPredicate) return 1; + if (a.isPredicate && b.isPredicate) { + if (a.key < b.key) return -1; + if (a.key > b.key) return 1; + } + return a.idx - b.idx; + }); + + return indexed.map((entry) => entry.child); +} + +/** + * Translate a single predicate into a SQL fragment. + * + * Every value flows through a freshly-allocated `:f_` named bind var. + * Nothing from the request body is ever interpolated as a literal — the + * fragment carries identifiers (registry-validated) and operators + * (whitelisted), then references the bind name for each value. + * + * `set` and `notSet` emit `IS NULL` / `IS NOT NULL` with no bind value. + * `in` and `notIn` emit `IN (:f_0, :f_1, ...)`. `contains` and `notContains` + * emit `LIKE :f_0` and pre-bind the value with `%` wrapping. + */ +function renderPredicate( + predicate: MetricPredicate, + params: Record, + state: FilterRenderState, +): string { + const col = predicate.member; + const op = predicate.operator; + const values = predicate.values ?? []; + + switch (op) { + case "equals": + return `${col} = ${bindValue(values[0], params, state)}`; + case "notEquals": + return `${col} <> ${bindValue(values[0], params, state)}`; + case "gt": + return `${col} > ${bindValue(values[0], params, state)}`; + case "gte": + return `${col} >= ${bindValue(values[0], params, state)}`; + case "lt": + return `${col} < ${bindValue(values[0], params, state)}`; + case "lte": + return `${col} <= ${bindValue(values[0], params, state)}`; + case "in": { + const placeholders = values.map((v) => bindValue(v, params, state)); + return `${col} IN (${placeholders.join(", ")})`; + } + case "notIn": { + const placeholders = values.map((v) => bindValue(v, params, state)); + return `${col} NOT IN (${placeholders.join(", ")})`; + } + case "contains": { + const raw = values[0]; + if (typeof raw !== "string") { + throw new Error( + `Refusing to build SQL: filter operator "contains" requires a string value (got ${typeof raw}).`, + ); + } + return `${col} LIKE ${bindLikeValue(raw, params, state)}`; + } + case "notContains": { + const raw = values[0]; + if (typeof raw !== "string") { + throw new Error( + `Refusing to build SQL: filter operator "notContains" requires a string value (got ${typeof raw}).`, + ); + } + return `${col} NOT LIKE ${bindLikeValue(raw, params, state)}`; + } + case "set": + return `${col} IS NOT NULL`; + case "notSet": + return `${col} IS NULL`; + default: { + // Exhaustiveness — the operator union is closed; if this is reached + // the operator vocabulary widened without updating the switch. + const _exhaustive: never = op; + throw new Error( + `Refusing to build SQL: unhandled filter operator "${_exhaustive as string}".`, + ); + } + } +} + +/** + * Allocate a fresh `:f_` bind name for `value`, push the typed marker + * into `params`, and return the placeholder string. Bumps the counter. + */ +function bindValue( + value: string | number | undefined, + params: Record, + state: FilterRenderState, +): string { + if (value === undefined) { + throw new Error( + "Refusing to build SQL: filter predicate is missing a required value.", + ); + } + const name = `f_${state.counter}`; + state.counter += 1; + if (typeof value === "number") { + params[name] = sqlHelpers.number(value); + } else if (typeof value === "string") { + params[name] = sqlHelpers.string(value); + } else { + throw new Error( + `Refusing to build SQL: filter value must be a string or number (got ${typeof value}).`, + ); + } + return `:${name}`; +} + +/** + * Like {@link bindValue}, but wraps the value in `%...%` for `LIKE` / + * `NOT LIKE`. SQL wildcards in the user-supplied string remain in the value + * (matching the documented "contains" semantics) — escape-on-receive could + * be added later as an opt-in if customers request strict-substring matching. + */ +function bindLikeValue( + value: string, + params: Record, + state: FilterRenderState, +): string { + const name = `f_${state.counter}`; + state.counter += 1; + params[name] = sqlHelpers.string(`%${value}%`); + return `:${name}`; } /** @@ -482,24 +1111,31 @@ function renderDimensionClause( * * Reserved namespace `metric:` separates metric-view caches from query * caches. Phase 4 finalizes sort-before-hash composition with the full - * argsHash / executorKey discipline; Phase 2's incremental need is for the - * key to vary on dimensions + timeGrain so semantically distinct calls get + * argsHash / executorKey discipline; Phase 3's incremental need is for the + * key to vary on the structured filter so semantically distinct calls get * distinct cache entries. * - * Order-insensitive components (measures, dimensions) are sorted before - * hashing into the key string, matching the PRD's sort-before-hash invariant. + * Order-insensitive components are sorted before hashing into the key + * string, matching the PRD's sort-before-hash invariant: + * - measures: lexicographic sort + * - dimensions: lexicographic sort + * - filter: predicates inside each AND/OR group are stable-sorted by + * `(member, operator)`; group kind (`and` vs `or`) is preserved */ export function composeMetricCacheKey(input: { metricKey: string; measures: string[]; dimensions?: string[]; timeGrain?: string; + filter?: MetricFilter; format: string; executorKey: string; limit?: number; }): string[] { const sortedMeasures = [...input.measures].sort(); const sortedDimensions = [...(input.dimensions ?? [])].sort(); + const filterFingerprint = + input.filter !== undefined ? canonicalizeFilter(input.filter) : "_"; return [ "metric", input.metricKey, @@ -507,7 +1143,48 @@ export function composeMetricCacheKey(input: { sortedMeasures.join(","), sortedDimensions.join(","), input.timeGrain ?? "_", + filterFingerprint, typeof input.limit === "number" ? String(input.limit) : "_", input.executorKey, ]; } + +/** + * Produce a deterministic string fingerprint of the filter tree. + * + * The fingerprint sorts predicates within each AND/OR group by + * `(member, operator)` and recursively canonicalizes nested groups. Values + * are included verbatim so cache entries differ when the filter targets + * different values (`region in [EMEA]` vs `region in [APAC]` — different + * keys; `equals A` vs `equals B` — different keys), while order-insensitive + * predicate lists collapse to the same key. + */ +function canonicalizeFilter(node: MetricFilter): string { + if (node === null || typeof node !== "object") { + return "_"; + } + + if ("and" in node || "or" in node) { + const groupKey = "and" in node ? "and" : "or"; + const children = ( + node as { and?: ReadonlyArray } & { + or?: ReadonlyArray; + } + )[groupKey]; + + if (!Array.isArray(children) || children.length === 0) { + return `${groupKey}()`; + } + + const sorted = sortFilterChildren(children); + const childFingerprints = sorted.map(canonicalizeFilter); + return `${groupKey}(${childFingerprints.join(",")})`; + } + + // Leaf predicate. + const p = node as MetricPredicate; + const valuesPart = p.values + ? p.values.map((v) => `${typeof v}:${String(v)}`).join("|") + : ""; + return `p(${p.member}/${p.operator}/${valuesPart})`; +} diff --git a/packages/appkit/src/plugins/analytics/tests/metric.test.ts b/packages/appkit/src/plugins/analytics/tests/metric.test.ts index 52d711469..2622c0989 100644 --- a/packages/appkit/src/plugins/analytics/tests/metric.test.ts +++ b/packages/appkit/src/plugins/analytics/tests/metric.test.ts @@ -69,6 +69,22 @@ const REVENUE_REGISTRATION: MetricRegistration = { }, }; +/** + * Phase 3 fixture — adds a numeric dim (`deal_size`) and registered + * `knownDimensionTypes` so op⇄type compatibility tests can exercise both + * branches (range ops on numeric dim, string ops on string dim). + */ +const REVENUE_PHASE3_REGISTRATION: MetricRegistration = { + ...REVENUE_REGISTRATION, + knownDimensions: ["region", "segment", "created_at", "deal_size"], + knownDimensionTypes: { + region: "STRING", + segment: "STRING", + created_at: "TIMESTAMP", + deal_size: "DOUBLE", + }, +}; + describe("metric — pure helpers", () => { describe("makeMetricRequestSchema / validateMetricRequest", () => { test("accepts a request with a known measure", () => { @@ -116,7 +132,17 @@ describe("metric — pure helpers", () => { expect(() => validateMetricRequest(REVENUE_REGISTRATION, { measures: ["arr"], - // 'filter' is reserved for Phase 3; the strict() schema must reject it. + // 'someUnknownField' is not in the v1 contract and the strict() + // schema must reject it. (filter is now a Phase 3 field.) + someUnknownField: 123, + } as any), + ).toThrowError(); + }); + + test("rejects filter passed as a bare array (not a Predicate or { and }/{or} group)", () => { + expect(() => + validateMetricRequest(REVENUE_REGISTRATION, { + measures: ["arr"], filter: [{ member: "region", operator: "in", values: ["EMEA"] }], } as any), ).toThrowError(); @@ -242,7 +268,8 @@ describe("metric — pure helpers", () => { expect(statement).toBe( "SELECT MEASURE(arr) FROM appkit_demo.public.revenue_metrics", ); - expect(parameters).toEqual([]); + // No filter present → no bind params. + expect(parameters).toEqual({}); }); test("sorts measures lexicographically for deterministic SQL", () => { @@ -1031,3 +1058,721 @@ describe("AnalyticsPlugin — metric route handler", () => { expect(errorPayload.code).toBe("VALIDATION_ERROR"); }); }); + +// ============================================================================ +// Phase 3 — Filter spec (recursive AND/OR with 12 v1 operators) +// ============================================================================ + +describe("metric — filter translator", () => { + // Helper: render filter via buildMetricSql and return WHERE fragment + params. + function render( + filter: any, + registration: MetricRegistration = REVENUE_PHASE3_REGISTRATION, + ) { + const { statement, parameters } = buildMetricSql(registration, { + measures: ["arr"], + filter, + }); + // Pull just the WHERE portion (between ` WHERE ` and ` GROUP BY` / ` LIMIT` / end). + const match = statement.match(/ WHERE (.+?)( GROUP BY| LIMIT|$)/); + const where = match ? match[1] : null; + return { statement, where, parameters }; + } + + describe("operators (12 unit tests)", () => { + test("equals → ` = :f_0`", () => { + const { where, parameters } = render({ + member: "region", + operator: "equals", + values: ["EMEA"], + }); + expect(where).toBe("region = :f_0"); + expect(parameters).toEqual({ + f_0: { __sql_type: "STRING", value: "EMEA" }, + }); + }); + + test("notEquals → ` <> :f_0`", () => { + const { where, parameters } = render({ + member: "region", + operator: "notEquals", + values: ["EMEA"], + }); + expect(where).toBe("region <> :f_0"); + expect(parameters.f_0).toEqual({ __sql_type: "STRING", value: "EMEA" }); + }); + + test("in → ` IN (:f_0, :f_1, ...)`", () => { + const { where, parameters } = render({ + member: "region", + operator: "in", + values: ["EMEA", "APAC", "AMER"], + }); + expect(where).toBe("region IN (:f_0, :f_1, :f_2)"); + expect(parameters.f_0).toEqual({ __sql_type: "STRING", value: "EMEA" }); + expect(parameters.f_1).toEqual({ __sql_type: "STRING", value: "APAC" }); + expect(parameters.f_2).toEqual({ __sql_type: "STRING", value: "AMER" }); + }); + + test("notIn → ` NOT IN (:f_0, :f_1, ...)`", () => { + const { where, parameters } = render({ + member: "region", + operator: "notIn", + values: ["EMEA", "APAC"], + }); + expect(where).toBe("region NOT IN (:f_0, :f_1)"); + expect(Object.keys(parameters)).toHaveLength(2); + }); + + test("gt → ` > :f_0`", () => { + const { where, parameters } = render({ + member: "deal_size", + operator: "gt", + values: [10000], + }); + expect(where).toBe("deal_size > :f_0"); + expect(parameters.f_0).toEqual({ __sql_type: "NUMERIC", value: "10000" }); + }); + + test("gte → ` >= :f_0`", () => { + const { where } = render({ + member: "deal_size", + operator: "gte", + values: [5000], + }); + expect(where).toBe("deal_size >= :f_0"); + }); + + test("lt → ` < :f_0`", () => { + const { where } = render({ + member: "deal_size", + operator: "lt", + values: [100], + }); + expect(where).toBe("deal_size < :f_0"); + }); + + test("lte → ` <= :f_0`", () => { + const { where } = render({ + member: "deal_size", + operator: "lte", + values: [50000], + }); + expect(where).toBe("deal_size <= :f_0"); + }); + + test("contains → ` LIKE :f_0` (value wrapped in %...%)", () => { + const { where, parameters } = render({ + member: "region", + operator: "contains", + values: ["MEA"], + }); + expect(where).toBe("region LIKE :f_0"); + expect(parameters.f_0).toEqual({ __sql_type: "STRING", value: "%MEA%" }); + }); + + test("notContains → ` NOT LIKE :f_0`", () => { + const { where, parameters } = render({ + member: "region", + operator: "notContains", + values: ["test"], + }); + expect(where).toBe("region NOT LIKE :f_0"); + expect(parameters.f_0).toEqual({ + __sql_type: "STRING", + value: "%test%", + }); + }); + + test("set → ` IS NOT NULL` (no bind)", () => { + const { where, parameters } = render({ + member: "region", + operator: "set", + }); + expect(where).toBe("region IS NOT NULL"); + expect(parameters).toEqual({}); + }); + + test("notSet → ` IS NULL` (no bind)", () => { + const { where, parameters } = render({ + member: "region", + operator: "notSet", + }); + expect(where).toBe("region IS NULL"); + expect(parameters).toEqual({}); + }); + }); + + describe("AND/OR composition", () => { + test("flat AND group renders predicates joined by AND", () => { + const { where, parameters } = render({ + and: [ + { member: "region", operator: "equals", values: ["EMEA"] }, + { member: "segment", operator: "equals", values: ["Enterprise"] }, + ], + }); + expect(where).toBe("(region = :f_0 AND segment = :f_1)"); + expect(parameters.f_0.value).toBe("EMEA"); + expect(parameters.f_1.value).toBe("Enterprise"); + }); + + test("flat OR group renders predicates joined by OR", () => { + const { where } = render({ + or: [ + { member: "region", operator: "equals", values: ["EMEA"] }, + { member: "region", operator: "equals", values: ["APAC"] }, + ], + }); + // Sort-before-hash: same member+operator pair sorts stably; both are + // (region, equals). The OR fragment renders both predicates. + expect(where).toBe("(region = :f_0 OR region = :f_1)"); + }); + + test("AND-of-OR composes nested groups", () => { + const { where } = render({ + and: [ + { member: "region", operator: "in", values: ["EMEA", "APAC"] }, + { + or: [ + { member: "segment", operator: "equals", values: ["Enterprise"] }, + { member: "deal_size", operator: "gt", values: [50000] }, + ], + }, + ], + }); + expect(where).toContain("(region IN ("); + expect(where).toContain(" AND "); + expect(where).toContain("OR"); + }); + + test("OR-of-AND composes nested groups", () => { + const { where } = render({ + or: [ + { + and: [ + { member: "region", operator: "equals", values: ["EMEA"] }, + { member: "segment", operator: "equals", values: ["Enterprise"] }, + ], + }, + { member: "region", operator: "equals", values: ["APAC"] }, + ], + }); + // Outer is OR of (AND group, leaf predicate). + expect(where).toMatch(/^\(.+ OR .+\)$/); + expect(where).toContain(" AND "); + }); + + test("deeply nested mix of AND/OR (4 levels)", () => { + const { where, parameters } = render({ + and: [ + { + or: [ + { + and: [ + { member: "region", operator: "equals", values: ["EMEA"] }, + { + or: [ + { + member: "segment", + operator: "equals", + values: ["Enterprise"], + }, + ], + }, + ], + }, + ], + }, + ], + }); + // Single-leaf groups collapse; multi-leaf groups parenthesize. + expect(where).toBeTruthy(); + // All values are bound. + expect(parameters.f_0.value).toBe("EMEA"); + expect(parameters.f_1.value).toBe("Enterprise"); + }); + + test("empty AND/OR group emits no WHERE clause", () => { + const { statement, parameters } = buildMetricSql( + REVENUE_PHASE3_REGISTRATION, + { + measures: ["arr"], + filter: { and: [] }, + }, + ); + expect(statement).not.toContain("WHERE"); + expect(parameters).toEqual({}); + }); + }); + + describe("depth cap", () => { + test("rejects 9 levels of AND nesting (validator)", () => { + // Build 9-deep AND nesting: { and: [ { and: [ ... { equals } ] } ] } + let node: any = { + member: "region", + operator: "equals", + values: ["EMEA"], + }; + for (let i = 0; i < 9; i += 1) { + node = { and: [node] }; + } + expect(() => + validateMetricRequest(REVENUE_PHASE3_REGISTRATION, { + measures: ["arr"], + filter: node, + }), + ).toThrowError(/maximum depth/); + }); + + test("accepts exactly 8 levels of AND nesting (validator)", () => { + let node: any = { + member: "region", + operator: "equals", + values: ["EMEA"], + }; + for (let i = 0; i < 8; i += 1) { + node = { and: [node] }; + } + expect(() => + validateMetricRequest(REVENUE_PHASE3_REGISTRATION, { + measures: ["arr"], + filter: node, + }), + ).not.toThrow(); + }); + }); + + describe("parameterization safety (no values in rendered SQL)", () => { + test("string values do not appear verbatim in the SQL string", () => { + const sneaky = "EMEA' OR '1'='1"; + const { statement } = render({ + member: "region", + operator: "equals", + values: [sneaky], + }); + expect(statement).not.toContain(sneaky); + expect(statement).toContain(":f_0"); + }); + + test("numeric values do not appear verbatim in the SQL string", () => { + const { statement } = render({ + member: "deal_size", + operator: "gt", + values: [987654321], + }); + expect(statement).not.toContain("987654321"); + expect(statement).toContain(":f_0"); + }); + + test("LIKE wildcard is the only value transformation; the original string is not in SQL", () => { + const { statement } = render({ + member: "region", + operator: "contains", + values: ["dangerous%"], + }); + expect(statement).not.toContain("dangerous"); + expect(statement).toContain(":f_0"); + }); + + test("IN values are individually bound (not concatenated)", () => { + const { statement, parameters } = render({ + member: "region", + operator: "in", + values: ["A", "B", "C"], + }); + // No raw value appears in the SQL string. + expect(statement).not.toMatch(/region IN \([^:]/); + expect(Object.keys(parameters)).toHaveLength(3); + }); + + test("identifier names in SQL come from the registry, not the request", () => { + // Even if a hostile member somehow bypasses validation, the SQL + // constructor's identifier guard rejects it before SQL emission. + expect(() => + render({ + member: "region; DROP TABLE foo --", + operator: "equals", + values: ["x"], + }), + ).toThrowError(/not a valid identifier|unknown filter member/); + }); + }); + + describe("validator rejection cases", () => { + test("rejects an unknown member", () => { + expect(() => + validateMetricRequest(REVENUE_PHASE3_REGISTRATION, { + measures: ["arr"], + filter: { + member: "ghost", + operator: "equals", + values: ["x"], + }, + }), + ).toThrowError(/not a declared dimension/); + }); + + test("rejects an unknown operator", () => { + expect(() => + validateMetricRequest(REVENUE_PHASE3_REGISTRATION, { + measures: ["arr"], + filter: { + member: "region", + operator: "startsWith" as any, + values: ["E"], + }, + }), + ).toThrowError(/not one of/); + }); + + test("rejects gt on a string-typed dimension (op⇄type)", () => { + expect(() => + validateMetricRequest(REVENUE_PHASE3_REGISTRATION, { + measures: ["arr"], + filter: { + member: "region", + operator: "gt", + values: ["EMEA"], + }, + }), + ).toThrowError(/incompatible/); + }); + + test("rejects contains on a numeric-typed dimension (op⇄type)", () => { + expect(() => + validateMetricRequest(REVENUE_PHASE3_REGISTRATION, { + measures: ["arr"], + filter: { + member: "deal_size", + operator: "contains", + values: ["1000"], + }, + }), + ).toThrowError(/incompatible/); + }); + + test("rejects contains on a date-typed dimension (op⇄type)", () => { + expect(() => + validateMetricRequest(REVENUE_PHASE3_REGISTRATION, { + measures: ["arr"], + filter: { + member: "created_at", + operator: "contains", + values: ["2026"], + }, + }), + ).toThrowError(/incompatible/); + }); + + test("accepts gt on a date-typed dimension", () => { + expect(() => + validateMetricRequest(REVENUE_PHASE3_REGISTRATION, { + measures: ["arr"], + filter: { + member: "created_at", + operator: "gt", + values: ["2026-01-01"], + }, + }), + ).not.toThrow(); + }); + + test("rejects equals with zero values (cardinality)", () => { + expect(() => + validateMetricRequest(REVENUE_PHASE3_REGISTRATION, { + measures: ["arr"], + filter: { + member: "region", + operator: "equals", + values: [], + }, + }), + ).toThrowError(/exactly one value/); + }); + + test("rejects equals with multiple values (cardinality)", () => { + expect(() => + validateMetricRequest(REVENUE_PHASE3_REGISTRATION, { + measures: ["arr"], + filter: { + member: "region", + operator: "equals", + values: ["A", "B"], + }, + }), + ).toThrowError(/exactly one value/); + }); + + test("rejects in with empty values (cardinality)", () => { + expect(() => + validateMetricRequest(REVENUE_PHASE3_REGISTRATION, { + measures: ["arr"], + filter: { + member: "region", + operator: "in", + values: [], + }, + }), + ).toThrowError(/at least one value/); + }); + + test("rejects set with values (cardinality — must be absent)", () => { + expect(() => + validateMetricRequest(REVENUE_PHASE3_REGISTRATION, { + measures: ["arr"], + filter: { + member: "region", + operator: "set", + values: ["EMEA"], + }, + }), + ).toThrowError(/must not carry values/); + }); + + test("accepts set with no values", () => { + expect(() => + validateMetricRequest(REVENUE_PHASE3_REGISTRATION, { + measures: ["arr"], + filter: { + member: "region", + operator: "set", + }, + }), + ).not.toThrow(); + }); + + test("accepts notSet with empty values array", () => { + expect(() => + validateMetricRequest(REVENUE_PHASE3_REGISTRATION, { + measures: ["arr"], + filter: { + member: "region", + operator: "notSet", + values: [], + }, + }), + ).not.toThrow(); + }); + + test("falls open on op⇄type when registry has no type metadata", () => { + // Without knownDimensionTypes, the validator cannot enforce op⇄type + // and accepts any op on any registered dim (defense-in-depth — the + // SQL constructor still enforces identifier shape and registry + // membership). + expect(() => + validateMetricRequest(REVENUE_REGISTRATION, { + measures: ["arr"], + filter: { + member: "region", + operator: "gt", + values: ["EMEA"], + }, + }), + ).not.toThrow(); + }); + + test("rejects member at depth — nested filter with unknown member", () => { + expect(() => + validateMetricRequest(REVENUE_PHASE3_REGISTRATION, { + measures: ["arr"], + filter: { + and: [ + { member: "region", operator: "equals", values: ["EMEA"] }, + { member: "ghost", operator: "equals", values: ["X"] }, + ], + }, + }), + ).toThrowError(/not a declared dimension/); + }); + }); + + describe("sort-before-hash (predicate ordering inside groups)", () => { + test("predicate order does not affect the rendered SQL within an AND group", () => { + const a = render({ + and: [ + { member: "region", operator: "equals", values: ["EMEA"] }, + { member: "segment", operator: "equals", values: ["Ent"] }, + ], + }); + const b = render({ + and: [ + { member: "segment", operator: "equals", values: ["Ent"] }, + { member: "region", operator: "equals", values: ["EMEA"] }, + ], + }); + // The bind-var indices may differ but the textual fragment shape + // sorts predicates by (member, operator), so both calls render the + // same WHERE clause. + expect(a.where).toBe(b.where); + }); + + test("predicate order does not affect cache key", () => { + const a = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + format: "JSON", + executorKey: "sp", + filter: { + and: [ + { member: "region", operator: "equals", values: ["EMEA"] }, + { member: "segment", operator: "equals", values: ["Ent"] }, + ], + }, + }); + const b = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + format: "JSON", + executorKey: "sp", + filter: { + and: [ + { member: "segment", operator: "equals", values: ["Ent"] }, + { member: "region", operator: "equals", values: ["EMEA"] }, + ], + }, + }); + expect(a).toEqual(b); + }); + + test("differentiates filtered vs unfiltered cache keys", () => { + const filtered = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + format: "JSON", + executorKey: "sp", + filter: { + member: "region", + operator: "equals", + values: ["EMEA"], + }, + }); + const unfiltered = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + format: "JSON", + executorKey: "sp", + }); + expect(filtered).not.toEqual(unfiltered); + }); + + test("differentiates filters with different values", () => { + const a = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + format: "JSON", + executorKey: "sp", + filter: { + member: "region", + operator: "equals", + values: ["EMEA"], + }, + }); + const b = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + format: "JSON", + executorKey: "sp", + filter: { + member: "region", + operator: "equals", + values: ["APAC"], + }, + }); + expect(a).not.toEqual(b); + }); + }); + + describe("route handler — filter integration", () => { + let serviceContextMock: Awaited>; + beforeEach(async () => { + setupDatabricksEnv(); + mockCacheStore.clear(); + ServiceContext.reset(); + serviceContextMock = await mockServiceContext(); + }); + afterEach(() => { + serviceContextMock?.restore(); + }); + + test("constructs WHERE clause from a structured filter", async () => { + const plugin = new AnalyticsPlugin({ timeout: 5000 }); + plugin._setMetricRegistryForTesting({ + revenue: REVENUE_PHASE3_REGISTRATION, + }); + const { router, getHandler } = createMockRouter(); + + const executeMock = vi.fn().mockResolvedValue({ + result: { data: [{ arr: 1234567 }] }, + }); + (plugin as any).SQLClient.executeStatement = executeMock; + + plugin.injectRoutes(router); + + const handler = getHandler("POST", "/metric/:key"); + const mockReq = createMockRequest({ + params: { key: "revenue" }, + body: { + measures: ["arr"], + filter: { + member: "region", + operator: "in", + values: ["EMEA", "APAC"], + }, + }, + }); + const mockRes = createMockResponse(); + + await handler(mockReq, mockRes); + + expect(executeMock).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + statement: expect.stringContaining("WHERE region IN (:f_0, :f_1)"), + parameters: expect.arrayContaining([ + expect.objectContaining({ + name: "f_0", + value: "EMEA", + type: "STRING", + }), + expect.objectContaining({ + name: "f_1", + value: "APAC", + type: "STRING", + }), + ]), + }), + expect.any(AbortSignal), + ); + }); + + test("returns 400 with the canonical error shape on filter rejection", async () => { + const plugin = new AnalyticsPlugin({ timeout: 5000 }); + plugin._setMetricRegistryForTesting({ + revenue: REVENUE_PHASE3_REGISTRATION, + }); + const { router, getHandler } = createMockRouter(); + plugin.injectRoutes(router); + + const handler = getHandler("POST", "/metric/:key"); + const mockReq = createMockRequest({ + params: { key: "revenue" }, + body: { + measures: ["arr"], + filter: { + member: "ghost", + operator: "equals", + values: ["X"], + }, + }, + }); + const mockRes = createMockResponse(); + + await handler(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(400); + const errorPayload = (mockRes.json as any).mock.calls[0][0]; + expect(errorPayload.code).toBe("VALIDATION_ERROR"); + expect(errorPayload.error).toMatch(/not a declared dimension/); + }); + }); +}); diff --git a/packages/appkit/src/plugins/analytics/types.ts b/packages/appkit/src/plugins/analytics/types.ts index 0f8946114..52bad4664 100644 --- a/packages/appkit/src/plugins/analytics/types.ts +++ b/packages/appkit/src/plugins/analytics/types.ts @@ -57,13 +57,82 @@ export interface MetricRegistration { * on requests for this metric. */ knownTimeGrainsByDim: Record; + /** + * Map of dimension name → registered SQL type (Phase 3). Drives op-vs-type + * compatibility checks in the filter validator (string ops on string-typed + * dims, range ops on numeric/date-typed dims). Empty map means + * "compatibility checks fall open"; the dimension still passes the + * identifier guard and the registry-membership check. + */ + knownDimensionTypes?: Record; +} + +/** + * Coarse classification of a dimension's column type, used by the filter + * validator to enforce op-vs-type compatibility. + * + * - `string` — STRING / VARCHAR / CHAR / TEXT (accepts string ops) + * - `numeric` — INT / BIGINT / DOUBLE / DECIMAL / etc (accepts range ops) + * - `date` — DATE / TIMESTAMP (accepts range ops) + * - `unknown` — fall-open: validator only enforces structural rules + */ +export type MetricDimensionTypeClass = + | "string" + | "numeric" + | "date" + | "unknown"; + +/** + * A single filter predicate — leaf node of the recursive {@link MetricFilter}. + * + * Server-side `IAnalyticsMetricRequest` uses the structural shape (no + * registry generic); the per-metric narrowing lives client-side via + * `Predicate` in `@databricks/appkit-ui/react`. + */ +export interface MetricPredicate { + member: string; + operator: MetricFilterOperatorName; + values?: ReadonlyArray; } /** - * Body of POST /api/analytics/metric/:key at Phase 2. + * The recursive filter type for the metric-view request body. + * + * Server-side use of this shape is intentionally non-generic — the registry + * generic only affects compile-time autocomplete and lives in + * `@databricks/appkit-ui/react`. + */ +export type MetricFilter = + | MetricPredicate + | { and: ReadonlyArray } + | { or: ReadonlyArray }; + +/** + * v1 filter operator vocabulary — exactly twelve names. Mirrored on the + * client as `MetricFilterOperator` in `@databricks/appkit-ui/react`. The + * runtime tuple `METRIC_FILTER_OPERATORS` lives next to the validator in + * `metric.ts`. + */ +export type MetricFilterOperatorName = + | "equals" + | "notEquals" + | "in" + | "notIn" + | "gt" + | "gte" + | "lt" + | "lte" + | "contains" + | "notContains" + | "set" + | "notSet"; + +/** + * Body of POST /api/analytics/metric/:key at Phase 3. * - * Phase 1 shape: `{ measures, format?, limit? }`. Phase 2 widens with - * `dimensions: string[]` and optional `timeGrain`. Phase 3 will add `filter`. + * Phase 1 shape: `{ measures, format?, limit? }`. Phase 2 added + * `dimensions: string[]` and optional `timeGrain`. Phase 3 adds optional + * structured `filter`. */ export interface IAnalyticsMetricRequest { measures: string[]; @@ -79,6 +148,12 @@ export interface IAnalyticsMetricRequest { * metric view's allowed grain enum (400). */ timeGrain?: string; + /** + * Structured filter expression — recursive AND/OR composition of predicates. + * All values are bound as parameters via the existing Statement Execution + * bind-var path; no value flows into the rendered SQL string. + */ + filter?: MetricFilter; format?: AnalyticsFormat; /** Optional row cap. */ limit?: number; From 600f6b24780dc532c838a9e1026dde439d1d9b7e Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Thu, 30 Apr 2026 00:05:40 +0200 Subject: [PATCH 04/34] =?UTF-8?q?feat(appkit):=20metric=20view=20source=20?= =?UTF-8?q?=E2=80=94=20Phase=204=20OBO=20lane=20+=20cache=20key=20composit?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Activate the OBO execution lane in metric.json (Phase 1 reserved the schema slot but only SP-lane was wired) and finalize the cache-key composition with full sort-before-hash. Server: _handleMetricRoute now dispatches via this.asUser(req) when the metric registration's lane is "obo", reusing the existing Plugin.asUser Proxy that .obo.sql files already use. New deriveMetricExecutorKey helper returns the literal "sp" for SP-lane entries and a sha256 hex digest of the user identity for OBO-lane entries; the raw x-forwarded-user value is hashed at the boundary and never reaches the cache key, telemetry, or error messages. Empty/whitespace identities collapse to an "anonymous" sentinel before hashing to prevent silent shared-cache scope across unauthenticated callers. Cache key invariants finalized: same args in any order produce the same key (sort-before-hash on measures, dimensions, and filter predicates within each AND/OR group); different args, lanes, or users produce different keys; SP literal "sp" cannot collide with an OBO user named "sp" because OBO identities are always hashed. dev-playground gains an OBO sample entry (customer_metrics) alongside Phase 1's SP entry (revenue). Tests: 1958 total (+22 from Phase 3's 1936). Coverage: deriveMetricExecutorKey (SP literal, OBO digest stability + uniqueness + privacy + sentinel), cache key invariants (8 tests), full route handler integration (cross-user, cross- lane, cache hit, default 1-hour TTL, registry rejects duplicate keys cross- lane). Backpressure (build, docs, check:fix, typecheck, test, knip) all green. Per prd/analytics-metric-view-source and tasks/.../Phase 4. xavier loop iteration 5. Co-authored-by: Isaac Signed-off-by: Atila Fassina --- .../dev-playground/config/queries/metric.json | 6 +- .../appkit/src/plugins/analytics/analytics.ts | 34 +- .../appkit/src/plugins/analytics/metric.ts | 69 ++- .../plugins/analytics/tests/metric.test.ts | 434 ++++++++++++++++++ 4 files changed, 522 insertions(+), 21 deletions(-) diff --git a/apps/dev-playground/config/queries/metric.json b/apps/dev-playground/config/queries/metric.json index 86fca1436..05d1218c0 100644 --- a/apps/dev-playground/config/queries/metric.json +++ b/apps/dev-playground/config/queries/metric.json @@ -5,5 +5,9 @@ "source": "appkit_demo.public.revenue_metrics" } }, - "obo": {} + "obo": { + "customer_metrics": { + "source": "appkit_demo.public.customer_metrics" + } + } } diff --git a/packages/appkit/src/plugins/analytics/analytics.ts b/packages/appkit/src/plugins/analytics/analytics.ts index 43f6aea40..2751d8434 100644 --- a/packages/appkit/src/plugins/analytics/analytics.ts +++ b/packages/appkit/src/plugins/analytics/analytics.ts @@ -17,6 +17,7 @@ import manifest from "./manifest.json"; import { buildMetricSql, composeMetricCacheKey, + deriveMetricExecutorKey, loadMetricRegistry, validateMetricRequest, } from "./metric"; @@ -253,16 +254,21 @@ export class AnalyticsPlugin extends Plugin { /** * Handle a metric-view query against `POST /api/analytics/metric/:key`. * - * Phase 1 surface: - * - body validated by zod (rejects unknown measures when the registry - * has build-time metadata) - * - SQL constructed as `SELECT MEASURE() FROM [LIMIT n]` + * Phase 4 surface: + * - body validated by zod (rejects unknown measures, dimensions, + * operators, and timeGrain values per the registry's build-time + * metadata) + * - SQL constructed via {@link buildMetricSql} with sorted SELECT list, + * parameterized filter, and `GROUP BY ALL` when dimensions are present * - response uses the same SSE envelope as the existing query route * - reuses the interceptor chain via `executeStream()` (telemetry, - * timeout, retry, cache) - * - * OBO dispatch is implemented but only the SP lane has callers in Phase 1. - * Phase 4 finalizes OBO + cache key composition. + * timeout, retry, cache) — default 1-hour TTL via `queryDefaults` + * - OBO dispatch: `lane === "obo"` entries route through `this.asUser(req)`, + * same Proxy pattern that `.obo.sql` files use today; SP entries route + * through the plugin's default executor. + * - Cache executor key: `"sp"` for SP-lane entries; sha256 hash of the + * user identity for OBO entries (raw `x-forwarded-user` value never + * reaches the cache layer — see {@link deriveMetricExecutorKey}). */ async _handleMetricRoute( req: express.Request, @@ -309,8 +315,18 @@ export class AnalyticsPlugin extends Plugin { const format = request.format ?? "JSON"; const isAsUser = registration.lane === "obo"; + // OBO lane: dispatch via the existing asUser(req) Proxy — same pattern + // used by .obo.sql files in `_handleQueryRoute`. The Proxy threads the + // user's `x-forwarded-access-token` through every Databricks call so + // the warehouse executes the query under the end user's identity. const executor = isAsUser ? this.asUser(req) : this; - const executorKey = isAsUser ? this.resolveUserId(req) : "sp"; + // OBO cache key: hash the user identity so the raw email/principal name + // never reaches the cache layer. SP cache key: literal "sp" — the cache + // is shared across every caller of the SP-lane metric. + const executorKey = deriveMetricExecutorKey({ + lane: registration.lane, + userIdentity: isAsUser ? this.resolveUserId(req) : null, + }); const queryParameters = format === "ARROW" diff --git a/packages/appkit/src/plugins/analytics/metric.ts b/packages/appkit/src/plugins/analytics/metric.ts index 2fced8c8d..aae29827b 100644 --- a/packages/appkit/src/plugins/analytics/metric.ts +++ b/packages/appkit/src/plugins/analytics/metric.ts @@ -1,3 +1,4 @@ +import { createHash } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; import { type SQLTypeMarker, sql as sqlHelpers } from "shared"; @@ -1107,20 +1108,31 @@ function renderDimensionClause( } /** - * Compose the cache key. + * Compose the cache key — final Phase 4 form. * * Reserved namespace `metric:` separates metric-view caches from query - * caches. Phase 4 finalizes sort-before-hash composition with the full - * argsHash / executorKey discipline; Phase 3's incremental need is for the - * key to vary on the structured filter so semantically distinct calls get - * distinct cache entries. + * caches. The key shape is `metric:{metric_key}:{argsHash}:{executorKey}`, + * where: + * - `metric_key` is the registry's stable map key (readable in debug logs). + * - `argsHash` is a deterministic serialization of the request body's + * canonical form. Order-insensitive components are sorted before they + * contribute to the hash so semantically equivalent calls collapse to the + * same cache entry. + * - `executorKey` is `"sp"` for SP-lane entries and a sha256 hash of the + * end-user's identity for OBO-lane entries. The raw identity is never + * placed in the cache key (privacy concern: cache stores log keys). * - * Order-insensitive components are sorted before hashing into the key - * string, matching the PRD's sort-before-hash invariant: - * - measures: lexicographic sort - * - dimensions: lexicographic sort - * - filter: predicates inside each AND/OR group are stable-sorted by - * `(member, operator)`; group kind (`and` vs `or`) is preserved + * Sort-before-hash applies to: + * - `measures`: lexicographic sort + * - `dimensions`: lexicographic sort + * - `filter`: predicates inside each AND/OR group are stable-sorted by + * `(member, operator)`; group kind (`and` vs `or`) is preserved by + * {@link canonicalizeFilter} + * + * The returned array is consumed by `CacheManager.generateKey` which + * concatenates and sha256-hashes the parts. The structure (one element per + * concern) makes the cache key inspectable in tests and debug logs without + * giving up determinism. */ export function composeMetricCacheKey(input: { metricKey: string; @@ -1149,6 +1161,41 @@ export function composeMetricCacheKey(input: { ]; } +/** + * Derive the cache executor key from a metric registration's lane and the + * caller's user identity. + * + * Returns `"sp"` for SP-lane entries (every caller shares the cache) and a + * sha256 hex digest of the user identity for OBO-lane entries (each user + * gets an isolated cache scope). + * + * The user identity is hashed — never stored verbatim — so the cache layer + * (which logs keys at debug level and persists them in any cache backend) + * never sees raw user emails or principal names. A stable, opaque token is + * what we need: same user → same key (so cache hits work), different users + * → different keys (so isolation holds), and reverse lookup is infeasible. + * + * For a missing or empty identity, falls back to a literal `"anonymous"` + * sentinel rather than an empty string. Empty-string hashes would collide + * across all callers without an identity — which is the bug a privacy-aware + * design must prevent. + */ +export function deriveMetricExecutorKey(input: { + lane: MetricLane; + userIdentity?: string | null; +}): string { + if (input.lane === "sp") { + return "sp"; + } + // OBO lane — hash the user identity so the raw email/principal never + // reaches the cache layer. `anonymous` is a sentinel for when the request + // has no resolvable identity (in practice this should not happen because + // OBO requires `x-forwarded-user`, but we belt-and-suspender it here). + const identity = input.userIdentity?.trim(); + const subject = identity && identity.length > 0 ? identity : "anonymous"; + return createHash("sha256").update(subject).digest("hex"); +} + /** * Produce a deterministic string fingerprint of the filter tree. * diff --git a/packages/appkit/src/plugins/analytics/tests/metric.test.ts b/packages/appkit/src/plugins/analytics/tests/metric.test.ts index 2622c0989..fb59b4c22 100644 --- a/packages/appkit/src/plugins/analytics/tests/metric.test.ts +++ b/packages/appkit/src/plugins/analytics/tests/metric.test.ts @@ -11,6 +11,7 @@ import { AnalyticsPlugin } from "../analytics"; import { buildMetricSql, composeMetricCacheKey, + deriveMetricExecutorKey, loadMetricRegistry, makeMetricRequestSchema, validateMetricRequest, @@ -1776,3 +1777,436 @@ describe("metric — filter translator", () => { }); }); }); + +// ============================================================================ +// Phase 4 — OBO lane + cache key composition (final form) +// +// Activates the OBO execution lane and finalizes cache-key composition. The +// cache executor key for OBO entries is a sha256 hash of the user identity — +// the raw header value never reaches the cache layer (privacy). Cross-user +// isolation, cross-lane isolation, and sort-before-hash on measures and +// dimensions are exercised here. +// ============================================================================ + +const CUSTOMER_OBO_REGISTRATION: MetricRegistration = { + key: "customer_metrics", + source: "appkit_demo.public.customer_metrics", + lane: "obo", + knownMeasures: ["churn_rate", "arpu"], + knownDimensions: ["csm_email", "region"], + knownTimeGrainsByDim: {}, +}; + +describe("metric — Phase 4 cache executor key", () => { + describe("deriveMetricExecutorKey", () => { + test("returns the literal 'sp' for SP-lane entries", () => { + const key = deriveMetricExecutorKey({ lane: "sp" }); + expect(key).toBe("sp"); + }); + + test("ignores userIdentity for SP-lane entries (caller cannot escalate)", () => { + // Even if a caller passes a userIdentity for an SP-lane entry, the + // function must return "sp" — SP-lane caches are inherently shared. + const key = deriveMetricExecutorKey({ + lane: "sp", + userIdentity: "alice@example.com", + }); + expect(key).toBe("sp"); + }); + + test("returns a sha256 hex digest for OBO-lane entries", () => { + const key = deriveMetricExecutorKey({ + lane: "obo", + userIdentity: "alice@example.com", + }); + // sha256 hex digest is 64 chars long. + expect(key).toMatch(/^[0-9a-f]{64}$/); + }); + + test("OBO digest is stable across calls for the same identity", () => { + const a = deriveMetricExecutorKey({ + lane: "obo", + userIdentity: "alice@example.com", + }); + const b = deriveMetricExecutorKey({ + lane: "obo", + userIdentity: "alice@example.com", + }); + expect(a).toBe(b); + }); + + test("OBO digests differ for different identities", () => { + const alice = deriveMetricExecutorKey({ + lane: "obo", + userIdentity: "alice@example.com", + }); + const bob = deriveMetricExecutorKey({ + lane: "obo", + userIdentity: "bob@example.com", + }); + expect(alice).not.toBe(bob); + }); + + test("does not contain the raw user identity (privacy)", () => { + // The hash output must not include the raw email — the whole point of + // hashing is that the cache layer (which logs keys) never sees PII. + const identity = "alice@example.com"; + const key = deriveMetricExecutorKey({ + lane: "obo", + userIdentity: identity, + }); + expect(key).not.toContain(identity); + expect(key).not.toContain("alice"); + expect(key).not.toContain("@"); + }); + + test("OBO-lane null identity falls back to anonymous sentinel", () => { + const a = deriveMetricExecutorKey({ lane: "obo", userIdentity: null }); + const b = deriveMetricExecutorKey({ + lane: "obo", + userIdentity: undefined, + }); + const c = deriveMetricExecutorKey({ lane: "obo", userIdentity: "" }); + const d = deriveMetricExecutorKey({ lane: "obo", userIdentity: " " }); + // All map to the same sentinel hash. + expect(a).toBe(b); + expect(b).toBe(c); + expect(c).toBe(d); + expect(a).toMatch(/^[0-9a-f]{64}$/); + }); + + test("OBO sentinel hash differs from any real identity hash", () => { + const sentinel = deriveMetricExecutorKey({ + lane: "obo", + userIdentity: undefined, + }); + const realUser = deriveMetricExecutorKey({ + lane: "obo", + userIdentity: "alice@example.com", + }); + expect(sentinel).not.toBe(realUser); + }); + + test("SP key differs from any OBO key (cross-lane isolation)", () => { + const sp = deriveMetricExecutorKey({ lane: "sp" }); + const obo = deriveMetricExecutorKey({ + lane: "obo", + userIdentity: "alice@example.com", + }); + expect(sp).not.toBe(obo); + }); + }); + + describe("composeMetricCacheKey — Phase 4 invariants", () => { + test("same args, different measure order → same key", () => { + const a = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr", "mrr"], + format: "JSON", + executorKey: "sp", + }); + const b = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["mrr", "arr"], + format: "JSON", + executorKey: "sp", + }); + expect(a).toEqual(b); + }); + + test("same args, different dimension order → same key", () => { + const a = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + dimensions: ["region", "segment"], + format: "JSON", + executorKey: "sp", + }); + const b = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + dimensions: ["segment", "region"], + format: "JSON", + executorKey: "sp", + }); + expect(a).toEqual(b); + }); + + test("same args, different filter predicate order → same key", () => { + const a = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + format: "JSON", + executorKey: "sp", + filter: { + and: [ + { member: "region", operator: "equals", values: ["EMEA"] }, + { member: "segment", operator: "equals", values: ["Ent"] }, + ], + }, + }); + const b = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + format: "JSON", + executorKey: "sp", + filter: { + and: [ + { member: "segment", operator: "equals", values: ["Ent"] }, + { member: "region", operator: "equals", values: ["EMEA"] }, + ], + }, + }); + expect(a).toEqual(b); + }); + + test("different args → different key", () => { + const a = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + format: "JSON", + executorKey: "sp", + }); + const b = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["mrr"], + format: "JSON", + executorKey: "sp", + }); + expect(a).not.toEqual(b); + }); + + test("SP vs OBO same args → different keys (cross-lane isolation)", () => { + const sp = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + format: "JSON", + executorKey: deriveMetricExecutorKey({ lane: "sp" }), + }); + const obo = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + format: "JSON", + executorKey: deriveMetricExecutorKey({ + lane: "obo", + userIdentity: "alice@example.com", + }), + }); + expect(sp).not.toEqual(obo); + }); + + test("OBO different users → different keys (cross-user isolation)", () => { + const alice = composeMetricCacheKey({ + metricKey: "customer_metrics", + measures: ["churn_rate"], + format: "JSON", + executorKey: deriveMetricExecutorKey({ + lane: "obo", + userIdentity: "alice@example.com", + }), + }); + const bob = composeMetricCacheKey({ + metricKey: "customer_metrics", + measures: ["churn_rate"], + format: "JSON", + executorKey: deriveMetricExecutorKey({ + lane: "obo", + userIdentity: "bob@example.com", + }), + }); + expect(alice).not.toEqual(bob); + }); + + test("OBO same user, same args → same key (cache hit)", () => { + const a = composeMetricCacheKey({ + metricKey: "customer_metrics", + measures: ["churn_rate"], + format: "JSON", + executorKey: deriveMetricExecutorKey({ + lane: "obo", + userIdentity: "alice@example.com", + }), + }); + const b = composeMetricCacheKey({ + metricKey: "customer_metrics", + measures: ["churn_rate"], + format: "JSON", + executorKey: deriveMetricExecutorKey({ + lane: "obo", + userIdentity: "alice@example.com", + }), + }); + expect(a).toEqual(b); + }); + + test("the raw user identity is not present in the cache key (privacy)", () => { + const identity = "alice@example.com"; + const key = composeMetricCacheKey({ + metricKey: "customer_metrics", + measures: ["churn_rate"], + format: "JSON", + executorKey: deriveMetricExecutorKey({ + lane: "obo", + userIdentity: identity, + }), + }); + // Inspect every part — none should contain the raw identity. + for (const part of key) { + expect(part).not.toContain(identity); + expect(part).not.toContain("alice"); + expect(part).not.toContain("@example.com"); + } + }); + }); +}); + +describe("AnalyticsPlugin — Phase 4 OBO + cache executor key", () => { + let serviceContextMock: Awaited>; + + beforeEach(async () => { + setupDatabricksEnv(); + mockCacheStore.clear(); + ServiceContext.reset(); + serviceContextMock = await mockServiceContext(); + }); + + afterEach(() => { + serviceContextMock?.restore(); + }); + + test("OBO lane: same args, different mock users → both queries execute (no cache leak)", async () => { + const plugin = new AnalyticsPlugin({ timeout: 5000 }); + plugin._setMetricRegistryForTesting({ + customer_metrics: CUSTOMER_OBO_REGISTRATION, + }); + const { router, getHandler } = createMockRouter(); + + const executeMock = vi + .fn() + .mockResolvedValueOnce({ + result: { data: [{ csm_email: "alice@x.com", churn_rate: 0.1 }] }, + }) + .mockResolvedValueOnce({ + result: { data: [{ csm_email: "bob@x.com", churn_rate: 0.2 }] }, + }); + (plugin as any).SQLClient.executeStatement = executeMock; + + plugin.injectRoutes(router); + + const handler = getHandler("POST", "/metric/:key"); + + const aliceReq = createMockRequest({ + params: { key: "customer_metrics" }, + body: { measures: ["churn_rate"] }, + headers: { + "x-forwarded-access-token": "alice-token", + "x-forwarded-user": "alice@example.com", + }, + }); + const aliceRes = createMockResponse(); + await handler(aliceReq, aliceRes); + + const bobReq = createMockRequest({ + params: { key: "customer_metrics" }, + body: { measures: ["churn_rate"] }, + headers: { + "x-forwarded-access-token": "bob-token", + "x-forwarded-user": "bob@example.com", + }, + }); + const bobRes = createMockResponse(); + await handler(bobReq, bobRes); + + // Different users, same query — the OBO cache must be partitioned per + // user, so both calls hit the warehouse. + expect(executeMock).toHaveBeenCalledTimes(2); + + // Each user sees their own row (no cache cross-contamination). + expect(aliceRes.write).toHaveBeenCalledWith( + expect.stringContaining("alice@x.com"), + ); + expect(bobRes.write).toHaveBeenCalledWith( + expect.stringContaining("bob@x.com"), + ); + }); + + test("OBO lane: same user, same args twice → second request hits cache", async () => { + const plugin = new AnalyticsPlugin({ timeout: 5000 }); + plugin._setMetricRegistryForTesting({ + customer_metrics: CUSTOMER_OBO_REGISTRATION, + }); + const { router, getHandler } = createMockRouter(); + + const executeMock = vi.fn().mockResolvedValue({ + result: { data: [{ csm_email: "alice@x.com", churn_rate: 0.1 }] }, + }); + (plugin as any).SQLClient.executeStatement = executeMock; + + plugin.injectRoutes(router); + + const handler = getHandler("POST", "/metric/:key"); + + const makeReq = () => + createMockRequest({ + params: { key: "customer_metrics" }, + body: { measures: ["churn_rate"] }, + headers: { + "x-forwarded-access-token": "alice-token", + "x-forwarded-user": "alice@example.com", + }, + }); + + await handler(makeReq(), createMockResponse()); + await handler(makeReq(), createMockResponse()); + + // Second request is served from cache. + expect(executeMock).toHaveBeenCalledTimes(1); + }); + + test("cross-lane isolation: SP user 'sp' literal does not collide with an OBO user named 'sp'", async () => { + // Defense-in-depth — the executor-key derivation must not let a user + // identity collide with the literal "sp" cache scope. Hashing the + // identity ensures this collision is structurally impossible. + const sp = deriveMetricExecutorKey({ lane: "sp" }); + const obo = deriveMetricExecutorKey({ + lane: "obo", + userIdentity: "sp", + }); + expect(sp).not.toBe(obo); + }); + + test("cache TTL defaults to 1 hour (3600 seconds) — matches existing analytics", async () => { + // The route handler builds its `defaultConfig` from `queryDefaults` — + // assert the TTL is unchanged so a future refactor that swaps defaults + // is caught by this test. + const { queryDefaults } = await import("../defaults"); + expect(queryDefaults.cache?.ttl).toBe(3600); + }); + + test("metric.json registry rejects same key in both sp and obo lanes (cross-lane key uniqueness)", async () => { + // Acceptance criterion 7: a metric key registered in both `sp` and + // `obo` is rejected at config-load time. Re-exercise the existing + // loader test here under the Phase 4 banner so the requirement is + // discoverable when reading Phase 4 tests. + const fs = await import("node:fs/promises"); + const os = await import("node:os"); + const path = await import("node:path"); + const tmpDir = await fs.mkdtemp( + path.join(os.tmpdir(), "appkit-metric-phase4-"), + ); + try { + await fs.writeFile( + path.join(tmpDir, "metric.json"), + JSON.stringify({ + sp: { revenue: { source: "demo.public.revenue" } }, + obo: { revenue: { source: "demo.public.revenue" } }, + }), + ); + await expect(loadMetricRegistry(undefined, tmpDir)).rejects.toThrowError( + /Duplicate metric key/, + ); + } finally { + await fs.rm(tmpDir, { recursive: true, force: true }); + } + }); +}); From 3f0c5368a911d8a664df15417a588d38ff031631 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Thu, 30 Apr 2026 00:30:01 +0200 Subject: [PATCH 05/34] =?UTF-8?q?feat(appkit-ui):=20metric=20view=20source?= =?UTF-8?q?=20=E2=80=94=20Phase=205=20metadata=20+=20format=20utilities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make UC YAML 1.1 semantic metadata (display names, format specs, descriptions) available to consumers via build-time bundling and library-agnostic format utilities. No changes to existing chart components — the BI story uses Plotly or comparable third-party libs and consumes metadata via the new helpers. Type-gen: extractMetricColumns now captures display_name (and camelCase displayName), format / format_spec, and description with tolerant parsers that read from direct fields, metadata., and the YAML's own attribute shapes. New buildMetricsMetadataBundle + generateMetricsMetadataJson emit shared/appkit-types/metrics.metadata.json next to metric.d.ts. The .d.ts adds a typed metadata field on each MetricRegistry entry so call-sites narrow on literal display_name / format / time_grain. Hook: useMetricView return shape extends to { data, metadata, loading, error }. metadata is the per-metric subset, available before data loads, stable across re-renders for the same key. Distribution via runtime registerMetricsMetadata(bundle) singleton — customers call once at startup because hooks can't statically import customer-side build artifacts. Format utilities at @databricks/appkit-ui/format: - formatValue(value, format?) — printf-like format string parser (currency prefix/suffix, percent, plain number); falls back to localized Intl.NumberFormat for unrecognized formats; tolerant for non-numeric and null/NaN - formatLabel(name, columnMetadata?) — display_name when present; camelCase / snake_case / SCREAMING_SNAKE / PascalCase humanization fallback - toD3Format(format?) — converts UC printf strings to d3-format-compatible strings ($,.2f, .1%, ,.0f); returns identity for unknown shapes so Plotly / ECharts can no-op gracefully Tests: 2041 total (+83 from Phase 4's 1958). Coverage: 18 type-gen tests, 48 format unit tests across all three utilities + library-agnostic Plotly / ECharts / table / KPI consumption flows, 8 registry tests, 9 hook metadata stability tests, 5 type-level narrowing tests. Per prd/analytics-metric-view-source and tasks/.../Phase 5. xavier loop iteration 6. Co-authored-by: Isaac Signed-off-by: Atila Fassina --- packages/appkit-ui/package.json | 5 + .../src/format/__tests__/format.test.ts | 269 ++++++++++++++ .../src/format/__tests__/registry.test.ts | 102 ++++++ packages/appkit-ui/src/format/format.ts | 321 ++++++++++++++++ packages/appkit-ui/src/format/index.ts | 32 ++ packages/appkit-ui/src/format/registry.ts | 78 ++++ packages/appkit-ui/src/format/types.ts | 71 ++++ .../use-metric-view-metadata.test.ts | 213 +++++++++++ .../__tests__/use-metric-view-types.test.ts | 70 ++++ .../hooks/__tests__/use-metric-view.test.ts | 4 + packages/appkit-ui/src/react/hooks/index.ts | 3 + packages/appkit-ui/src/react/hooks/types.ts | 102 +++++- .../src/react/hooks/use-metric-view.ts | 29 +- packages/appkit-ui/tsdown.config.ts | 1 + packages/appkit/src/type-generator/index.ts | 32 +- .../src/type-generator/metric-registry.ts | 240 ++++++++++++ .../metric-registry.test.ts.snap | 96 +++++ .../tests/metric-registry.test.ts | 342 ++++++++++++++++++ .../appkit/src/type-generator/vite-plugin.ts | 14 + 19 files changed, 2015 insertions(+), 9 deletions(-) create mode 100644 packages/appkit-ui/src/format/__tests__/format.test.ts create mode 100644 packages/appkit-ui/src/format/__tests__/registry.test.ts create mode 100644 packages/appkit-ui/src/format/format.ts create mode 100644 packages/appkit-ui/src/format/index.ts create mode 100644 packages/appkit-ui/src/format/registry.ts create mode 100644 packages/appkit-ui/src/format/types.ts create mode 100644 packages/appkit-ui/src/react/hooks/__tests__/use-metric-view-metadata.test.ts diff --git a/packages/appkit-ui/package.json b/packages/appkit-ui/package.json index e7e91d5d0..2105929cd 100644 --- a/packages/appkit-ui/package.json +++ b/packages/appkit-ui/package.json @@ -23,6 +23,10 @@ "sbom.cdx.json" ], "exports": { + "./format": { + "development": "./src/format/index.ts", + "default": "./dist/format/index.js" + }, "./js": { "development": "./src/js/index.ts", "default": "./dist/js/index.js" @@ -118,6 +122,7 @@ }, "publishConfig": { "exports": { + "./format": "./dist/format/index.js", "./js": "./dist/js/index.js", "./js/beta": "./dist/js/beta.js", "./react": "./dist/react/index.js", diff --git a/packages/appkit-ui/src/format/__tests__/format.test.ts b/packages/appkit-ui/src/format/__tests__/format.test.ts new file mode 100644 index 000000000..d1c48cd39 --- /dev/null +++ b/packages/appkit-ui/src/format/__tests__/format.test.ts @@ -0,0 +1,269 @@ +import { describe, expect, test } from "vitest"; +import { formatLabel, formatValue, toD3Format } from "../format"; + +/** + * Format utility tests for Phase 5 of the metric-view PRD. + * + * Coverage matrix per the task acceptance criteria: + * - formatValue: currency / percent / number / unknown-fallback / null cases + * - formatLabel: camelCase / snake_case / display-name-override + * - toD3Format: currency / percent / integer / unknown-fallback + */ + +describe("formatValue", () => { + test("formats currency with two decimals", () => { + expect(formatValue(1234.56, "$#,##0.00")).toBe("$1,234.56"); + }); + + test("formats currency with thousands separator", () => { + expect(formatValue(1234567.89, "$#,##0.00")).toBe("$1,234,567.89"); + }); + + test("formats negative currency with sign before symbol", () => { + expect(formatValue(-1234.56, "$#,##0.00")).toBe("-$1,234.56"); + }); + + test("formats zero currency correctly", () => { + expect(formatValue(0, "$#,##0.00")).toBe("$0.00"); + }); + + test("formats percent with one decimal", () => { + expect(formatValue(0.427, "0.0%")).toBe("42.7%"); + }); + + test("formats percent with no decimals", () => { + expect(formatValue(0.5, "0%")).toBe("50%"); + }); + + test("formats percent with two decimals", () => { + expect(formatValue(0.12345, "0.00%")).toBe("12.35%"); + }); + + test("formats integer with thousands separator", () => { + expect(formatValue(1234, "#,##0")).toBe("1,234"); + }); + + test("formats fixed-precision number", () => { + expect(formatValue(1.23456, "0.000")).toBe("1.235"); + }); + + test("formats number without grouping when format omits comma", () => { + expect(formatValue(1234, "0")).toBe("1234"); + }); + + test("falls back to localized formatting for unrecognized format spec", () => { + // Unknown spec → Intl.NumberFormat default. Just assert it's a non-empty + // string that contains the digits — locale-specific separators vary. + const result = formatValue(1234.5, "weird-spec-xyz"); + expect(typeof result).toBe("string"); + expect(result.length).toBeGreaterThan(0); + }); + + test("falls back to localized formatting when format is undefined", () => { + const result = formatValue(42); + expect(typeof result).toBe("string"); + expect(result).toContain("42"); + }); + + test("returns empty string for null value", () => { + expect(formatValue(null, "$#,##0.00")).toBe(""); + }); + + test("returns empty string for undefined value", () => { + expect(formatValue(undefined, "$#,##0.00")).toBe(""); + }); + + test("returns String(value) for string input regardless of format", () => { + expect(formatValue("EMEA", "$#,##0.00")).toBe("EMEA"); + }); + + test("returns String(value) for boolean input", () => { + expect(formatValue(true)).toBe("true"); + }); + + test("handles bigint input by converting to number", () => { + expect(formatValue(1234n, "#,##0")).toBe("1,234"); + }); + + test("returns String(NaN) when value is non-finite", () => { + expect(formatValue(Number.NaN)).toBe("NaN"); + expect(formatValue(Number.POSITIVE_INFINITY)).toBe("Infinity"); + }); + + test("recognizes suffix-style currency (e.g. 0.00 kr)", () => { + // Common Nordic format + expect(formatValue(1234.56, "#,##0.00 kr")).toContain("1,234.56"); + expect(formatValue(1234.56, "#,##0.00 kr")).toContain("kr"); + }); +}); + +describe("formatLabel", () => { + test("returns display_name from metadata when present", () => { + expect( + formatLabel("arr", { + type: "DECIMAL", + display_name: "Annual Recurring Revenue", + }), + ).toBe("Annual Recurring Revenue"); + }); + + test("falls back to humanization when display_name is absent", () => { + expect(formatLabel("arr", { type: "DECIMAL" })).toBe("Arr"); + }); + + test("falls back to humanization when metadata is undefined", () => { + expect(formatLabel("revenue")).toBe("Revenue"); + }); + + test("humanizes snake_case", () => { + expect(formatLabel("total_revenue")).toBe("Total Revenue"); + expect(formatLabel("user_name")).toBe("User Name"); + expect(formatLabel("annual_recurring_revenue")).toBe( + "Annual Recurring Revenue", + ); + }); + + test("humanizes camelCase", () => { + expect(formatLabel("customerCount")).toBe("Customer Count"); + expect(formatLabel("totalCost")).toBe("Total Cost"); + expect(formatLabel("annualRecurringRevenue")).toBe( + "Annual Recurring Revenue", + ); + }); + + test("humanizes PascalCase", () => { + expect(formatLabel("UserId")).toBe("User Id"); + expect(formatLabel("CustomerCount")).toBe("Customer Count"); + }); + + test("humanizes SCREAMING_SNAKE_CASE", () => { + expect(formatLabel("USER_ID")).toBe("User Id"); + expect(formatLabel("ANNUAL_REVENUE")).toBe("Annual Revenue"); + }); + + test("preserves already-spaced input with title-case normalization", () => { + expect(formatLabel("annual revenue")).toBe("Annual Revenue"); + }); + + test("ignores empty/whitespace display_name and falls back to humanization", () => { + expect(formatLabel("arr", { type: "DECIMAL", display_name: " " })).toBe( + "Arr", + ); + expect(formatLabel("arr", { type: "DECIMAL", display_name: "" })).toBe( + "Arr", + ); + }); + + test("strips dangerous non-identifier characters before humanizing", () => { + expect(formatLabel("user")).toBe( + "Userscriptnamescript", + ); + }); + + test("returns empty string for an empty input name", () => { + expect(formatLabel("")).toBe(""); + }); + + test("handles single-word lowercase identifier", () => { + expect(formatLabel("revenue")).toBe("Revenue"); + }); + + test("handles consecutive capitals (acronyms)", () => { + expect(formatLabel("ARRGrowth")).toBe("Arr Growth"); + }); +}); + +describe("toD3Format", () => { + test("converts currency with two decimals", () => { + expect(toD3Format("$#,##0.00")).toBe("$,.2f"); + }); + + test("converts currency with no decimals", () => { + expect(toD3Format("$#,##0")).toBe("$,.0f"); + }); + + test("converts percent with one decimal", () => { + expect(toD3Format("0.0%")).toBe(".1%"); + }); + + test("converts percent with two decimals", () => { + expect(toD3Format("0.00%")).toBe(".2%"); + }); + + test("converts percent with thousands separator", () => { + expect(toD3Format("#,##0.0%")).toBe(",.1%"); + }); + + test("converts integer with thousands separator", () => { + expect(toD3Format("#,##0")).toBe(",.0f"); + }); + + test("converts integer without thousands separator", () => { + expect(toD3Format("0")).toBe(".0f"); + }); + + test("converts fixed-precision number", () => { + expect(toD3Format("0.000")).toBe(".3f"); + }); + + test("falls back to identity for unrecognized format spec", () => { + expect(toD3Format("weird-spec-xyz")).toBe("weird-spec-xyz"); + }); + + test("returns empty string for undefined format", () => { + expect(toD3Format()).toBe(""); + }); + + test("returns empty string for empty format", () => { + expect(toD3Format("")).toBe(""); + }); + + test("treats already-d3 format as identity (acceptable: chart consumes it)", () => { + expect(toD3Format(".2f")).toBe(".2f"); + }); +}); + +// ── End-to-end utility flow: simulating chart consumption ──────────────── +describe("library-agnostic chart consumption flow", () => { + test("Plotly tickformat workflow: metadata → toD3Format → tickformat string", () => { + // Customer would do: { tickformat: toD3Format(metadata.measures.arr.format) } + const metadataFormat = "$#,##0.00"; + const tickformat = toD3Format(metadataFormat); + expect(tickformat).toBe("$,.2f"); + }); + + test("ECharts valueFormatter workflow: format function from metadata", () => { + const metadata = { + type: "DECIMAL", + display_name: "ARR", + format: "$#,##0.00", + }; + // ECharts valueFormatter receives raw values and returns strings. + const valueFormatter = (v: number) => formatValue(v, metadata.format); + expect(valueFormatter(1234.56)).toBe("$1,234.56"); + }); + + test("Table cell workflow: formatValue per row, formatLabel per column", () => { + const arrMetadata: import("../types").ColumnMetadata = { + type: "DECIMAL", + display_name: "Annual Recurring Revenue", + format: "$#,##0.00", + }; + const regionMetadata: import("../types").ColumnMetadata = { + type: "STRING", + }; + + expect(formatLabel("arr", arrMetadata)).toBe("Annual Recurring Revenue"); + expect(formatLabel("region", regionMetadata)).toBe("Region"); + expect(formatValue(1234.56, arrMetadata.format)).toBe("$1,234.56"); + // No format spec on the dimension; passes through value as-is. + expect(formatValue("EMEA", regionMetadata.format)).toBe("EMEA"); + }); + + test("KPI tile workflow: scalar value with optional unknown format", () => { + // Customer KPI tile is a single value display. + expect(formatValue(0.427, "0.0%")).toBe("42.7%"); + // Falls back gracefully when the metric YAML lacks a format spec. + expect(formatValue(0.427)).toBeTruthy(); + }); +}); diff --git a/packages/appkit-ui/src/format/__tests__/registry.test.ts b/packages/appkit-ui/src/format/__tests__/registry.test.ts new file mode 100644 index 000000000..9696f094d --- /dev/null +++ b/packages/appkit-ui/src/format/__tests__/registry.test.ts @@ -0,0 +1,102 @@ +import { afterEach, describe, expect, test } from "vitest"; +import { + _getRegisteredBundleForTesting, + clearMetricsMetadata, + getMetricMetadata, + registerMetricsMetadata, +} from "../registry"; +import type { MetricsMetadataBundle } from "../types"; + +afterEach(() => { + clearMetricsMetadata(); +}); + +const sampleBundle: MetricsMetadataBundle = { + revenue: { + source: "demo.public.revenue", + lane: "sp", + measures: { + arr: { + type: "DECIMAL(38,2)", + display_name: "Annual Recurring Revenue", + format: "$#,##0.00", + }, + }, + dimensions: { + region: { type: "STRING" }, + }, + }, + customer_metrics: { + source: "demo.public.customer_metrics", + lane: "obo", + measures: { + churn: { type: "DOUBLE", format: "0.0%" }, + }, + dimensions: { + csm_email: { type: "STRING" }, + }, + }, +}; + +describe("registerMetricsMetadata + getMetricMetadata", () => { + test("returns null for any key when no bundle has been registered", () => { + expect(getMetricMetadata("revenue")).toBeNull(); + }); + + test("returns the registered metadata for a known metric key", () => { + registerMetricsMetadata(sampleBundle); + const metadata = getMetricMetadata("revenue"); + expect(metadata).not.toBeNull(); + expect(metadata?.source).toBe("demo.public.revenue"); + expect(metadata?.measures.arr.format).toBe("$#,##0.00"); + }); + + test("returns null for an unregistered metric key", () => { + registerMetricsMetadata(sampleBundle); + expect(getMetricMetadata("nonexistent")).toBeNull(); + }); + + test("returns the same object reference on repeated lookups (stable identity)", () => { + registerMetricsMetadata(sampleBundle); + const ref1 = getMetricMetadata("revenue"); + const ref2 = getMetricMetadata("revenue"); + expect(ref1).toBe(ref2); + }); + + test("calling register replaces the previous bundle wholesale", () => { + registerMetricsMetadata(sampleBundle); + expect(getMetricMetadata("revenue")).not.toBeNull(); + + const newBundle: MetricsMetadataBundle = { + orders: { + source: "demo.public.orders", + lane: "sp", + measures: { count: { type: "BIGINT" } }, + dimensions: {}, + }, + }; + registerMetricsMetadata(newBundle); + expect(getMetricMetadata("revenue")).toBeNull(); + expect(getMetricMetadata("orders")).not.toBeNull(); + }); + + test("registering null clears the bundle", () => { + registerMetricsMetadata(sampleBundle); + expect(getMetricMetadata("revenue")).not.toBeNull(); + registerMetricsMetadata(null); + expect(getMetricMetadata("revenue")).toBeNull(); + }); + + test("clearMetricsMetadata resets the registry to unregistered state", () => { + registerMetricsMetadata(sampleBundle); + clearMetricsMetadata(); + expect(getMetricMetadata("revenue")).toBeNull(); + expect(_getRegisteredBundleForTesting()).toBeNull(); + }); + + test("supports both SP and OBO lanes in the same bundle", () => { + registerMetricsMetadata(sampleBundle); + expect(getMetricMetadata("revenue")?.lane).toBe("sp"); + expect(getMetricMetadata("customer_metrics")?.lane).toBe("obo"); + }); +}); diff --git a/packages/appkit-ui/src/format/format.ts b/packages/appkit-ui/src/format/format.ts new file mode 100644 index 000000000..d38b0f432 --- /dev/null +++ b/packages/appkit-ui/src/format/format.ts @@ -0,0 +1,321 @@ +import type { ColumnMetadata, FormatSpec } from "./types"; + +/** + * Library-agnostic format utilities for UC Metric View consumption. + * + * Phase 5 of the analytics-metric-view PRD: customers wire metric metadata + * into Plotly / ECharts / table cells / KPI tiles via these three helpers. + * No chart-library lock-in, no AppKit-specific chart prop — the utilities + * accept the YAML 1.1 format spec verbatim and produce strings the consumer + * passes into their chart of choice. + * + * Design decisions: + * - **Format-string passthrough.** UC YAML emits printf-style strings; we + * forward them. We do NOT design our own format DSL. Consumers see exactly + * what their data engineers wrote in the metric view spec. + * - **Tolerant fallbacks.** Unrecognized format strings fall back to + * sensible defaults (`Intl.NumberFormat` for `formatValue`, identity for + * `toD3Format`) rather than throwing. Charts continue to render even when + * the metric view's format spec uses an unsupported pattern. + * - **No `d3-format` dependency.** `toD3Format` is a pure string conversion + * — d3-format itself is the consumer (Plotly's tickformat, ECharts' + * valueFormatter, etc.). + * - **No null/undefined surprises.** All three helpers handle nullish + * inputs gracefully so chart code can pass values straight through + * without pre-checking. + */ + +/** + * Format a raw value into a display string per a YAML 1.1 format spec. + * + * When `format` is provided and recognized: + * - `$#,##0.00` style → currency (`"$1,234.56"`) + * - `#,##0.00` / `0.000` style → fixed-precision number (`"1,234.57"`) + * - `0.0%` / `#,##0%` style → percentage (`"42.7%"`) + * - `#,##0` style → integer with thousands separator (`"1,234"`) + * + * When `format` is unset / unrecognized / unparseable, falls back to: + * - localized number formatting via `Intl.NumberFormat` for numeric values + * - `String(value)` for non-numeric values + * + * Null / undefined input always returns the empty string — chart code can + * pass row cells straight through without pre-checking. + * + * @example + * formatValue(1234.56, "$#,##0.00") // "$1,234.56" + * formatValue(0.427, "0.0%") // "42.7%" + * formatValue(1234, "#,##0") // "1,234" + * formatValue(42, undefined) // "42" + * formatValue("EMEA", undefined) // "EMEA" + * formatValue(null, "$#,##0.00") // "" + */ +export function formatValue(value: unknown, format?: FormatSpec): string { + if (value == null) return ""; + + // Non-numeric values are returned as their string form regardless of format + // spec — the spec only makes sense for numeric output and the printf style + // does not have a defined meaning over strings/booleans/dates. + if (typeof value !== "number" && typeof value !== "bigint") { + return String(value); + } + + const numeric = typeof value === "bigint" ? Number(value) : value; + if (!Number.isFinite(numeric)) return String(numeric); + + const parsed = format ? parseFormatSpec(format) : null; + if (parsed == null) { + // No format / unrecognized format → localized number formatting. Using + // the user's locale (no explicit "en-US") so numbers render correctly in + // EU/JP/etc apps without the customer wiring locale plumbing. + return new Intl.NumberFormat(undefined, { + maximumFractionDigits: 6, + }).format(numeric); + } + + const { kind, fractionDigits, useGrouping, currencyPrefix, currencySuffix } = + parsed; + + switch (kind) { + case "percent": + return new Intl.NumberFormat(undefined, { + style: "percent", + minimumFractionDigits: fractionDigits, + maximumFractionDigits: fractionDigits, + useGrouping, + }).format(numeric); + case "currency": { + // We emit the currency symbol verbatim from the format spec rather than + // relying on `Intl.NumberFormat({ style: "currency", currency: "USD" })` + // — the YAML's `$#,##0.00` does not specify ISO currency code, and + // assuming USD would be wrong for non-US deployments. Passthrough lets + // data engineers pin the symbol they intend. + const numberPart = new Intl.NumberFormat(undefined, { + minimumFractionDigits: fractionDigits, + maximumFractionDigits: fractionDigits, + useGrouping, + }).format(Math.abs(numeric)); + const sign = numeric < 0 ? "-" : ""; + return `${sign}${currencyPrefix ?? ""}${numberPart}${currencySuffix ?? ""}`; + } + case "number": + return new Intl.NumberFormat(undefined, { + minimumFractionDigits: fractionDigits, + maximumFractionDigits: fractionDigits, + useGrouping, + }).format(numeric); + } +} + +/** + * Render a column's display label. + * + * Returns `display_name` from the metadata when present (the YAML 1.1 + * canonical label). When metadata is absent or `display_name` is missing, + * humanizes the column name: + * - snake_case (`total_revenue`) → "Total Revenue" + * - camelCase (`customerCount`) → "Customer Count" + * - PascalCase (`UserId`) → "User Id" + * - SCREAMING_SNAKE (`USER_ID`) → "User Id" + * - already-spaced (`Annual Recurring Revenue`) → unchanged (title-case) + * + * @example + * formatLabel("arr", { type: "DECIMAL", display_name: "Annual Recurring Revenue" }) + * // "Annual Recurring Revenue" + * formatLabel("total_revenue") // "Total Revenue" + * formatLabel("customerCount") // "Customer Count" + * formatLabel("revenue") // "Revenue" + */ +export function formatLabel( + name: string, + columnMetadata?: ColumnMetadata, +): string { + if ( + columnMetadata?.display_name && + columnMetadata.display_name.trim().length > 0 + ) { + return columnMetadata.display_name; + } + return humanizeIdentifier(name); +} + +/** + * Convert a UC YAML 1.1 printf-style format spec to a d3-format-compatible + * string. The output is consumed by Plotly's `tickformat`, ECharts' + * `valueFormatter`, table cell formatters, and any other library that + * understands d3-format syntax. + * + * Conversions: + * - `$#,##0.00` → `"$,.2f"` + * - `0.00%` → `".2%"` + * - `#,##0` → `",.0f"` + * - `0.000` → `".3f"` + * + * Unrecognized specs fall back to identity (the input string) so the chart + * library either consumes it directly (if it happens to be d3-format already) + * or surfaces its own warning. Returns the empty string for nullish input + * (chart libraries treat `""` as "use default"). + * + * @example + * toD3Format("$#,##0.00") // "$,.2f" + * toD3Format("0.0%") // ".1%" + * toD3Format("#,##0") // ",.0f" + * toD3Format(undefined) // "" + */ +export function toD3Format(format?: FormatSpec): string { + if (!format) return ""; + const parsed = parseFormatSpec(format); + if (parsed == null) { + // Unrecognized → identity. The consumer's chart library decides whether + // to consume it (e.g. Plotly silently ignores invalid tickformats) or to + // surface its own warning. We don't throw because chart libraries + // typically can't propagate exceptions out of their render path. + return format; + } + + const groupPart = parsed.useGrouping ? "," : ""; + switch (parsed.kind) { + case "currency": + // d3-format's `$` prefix is the standard "use locale's currency + // symbol" — most Plotly users want the YAML's literal symbol though. + // We emit `$` here so existing d3-format docs match; consumers that + // need a non-USD symbol pass `format` directly into their chart. + return `$${groupPart}.${parsed.fractionDigits}f`; + case "percent": + return `${groupPart}.${parsed.fractionDigits}%`; + case "number": + return `${groupPart}.${parsed.fractionDigits}f`; + } +} + +/** + * Parsed shape of a printf-style format spec. The parser is intentionally + * narrow: it recognizes the shapes UC documents (`$#,##0.00`, `0.0%`, + * `#,##0`, `0.000`, etc.) and returns null for anything else so callers can + * fall back to a sensible default. + * + * @internal + */ +interface ParsedFormat { + kind: "currency" | "percent" | "number"; + fractionDigits: number; + useGrouping: boolean; + /** Currency prefix (e.g. `"$"`, `"€"`) when the format starts with a symbol. */ + currencyPrefix?: string; + /** Currency suffix (e.g. `" kr"`) when the format ends with a non-digit token. */ + currencySuffix?: string; +} + +/** + * Recognize the small grammar of printf-style format specs we forward. + * + * Approach: strip percent / currency markers, count fractional digits via + * the substring after `.`, detect grouping via the presence of `,`. Anything + * not matching the recognized shape returns null. + */ +function parseFormatSpec(spec: FormatSpec): ParsedFormat | null { + const trimmed = spec.trim(); + if (trimmed.length === 0) return null; + + // Percent forms: `0.00%`, `#,##0%`, `0.0%`, `0%`. + const percentMatch = trimmed.match(/^([#,]*[0]+(?:\.[0]+)?)\s*%$/); + if (percentMatch) { + const numericPart = percentMatch[1]; + return { + kind: "percent", + fractionDigits: countFractionDigits(numericPart), + useGrouping: numericPart.includes(","), + }; + } + + // Currency forms: `$#,##0.00`, `€#,##0`, `$0.000`. Currency prefix is one + // or more leading non-digit/non-`#`/non-`,`/non-`.` characters, followed by + // the numeric portion. + const currencyPrefixMatch = trimmed.match(/^([^#,0.]+)([#,0.]+)$/); + if (currencyPrefixMatch) { + const prefix = currencyPrefixMatch[1]; + const numericPart = currencyPrefixMatch[2]; + if (isNumericFormat(numericPart)) { + return { + kind: "currency", + fractionDigits: countFractionDigits(numericPart), + useGrouping: numericPart.includes(","), + currencyPrefix: prefix, + }; + } + } + + // Suffix-symbol currency: `#,##0.00 kr`, `0.00 €`. Numeric portion first, + // suffix second (separated by a space or directly adjacent). + const currencySuffixMatch = trimmed.match(/^([#,0.]+)(\s*[^#,0.]+)$/); + if (currencySuffixMatch) { + const numericPart = currencySuffixMatch[1]; + const suffix = currencySuffixMatch[2]; + if (isNumericFormat(numericPart)) { + return { + kind: "currency", + fractionDigits: countFractionDigits(numericPart), + useGrouping: numericPart.includes(","), + currencySuffix: suffix, + }; + } + } + + // Plain number forms: `#,##0`, `#,##0.00`, `0.000`, `0`. + if (isNumericFormat(trimmed)) { + return { + kind: "number", + fractionDigits: countFractionDigits(trimmed), + useGrouping: trimmed.includes(","), + }; + } + + return null; +} + +/** + * Whether a string is a printf-numeric pattern of `#`, `0`, `,`, and `.`. + * A valid pattern has at least one digit placeholder (`0` or `#`). + */ +function isNumericFormat(s: string): boolean { + if (!/^[#,0.]+$/.test(s)) return false; + return /[0#]/.test(s); +} + +/** Count the number of `0` or `#` placeholders after the decimal point. */ +function countFractionDigits(s: string): number { + const dotIdx = s.indexOf("."); + if (dotIdx === -1) return 0; + const fractional = s.slice(dotIdx + 1); + // Fractional part should be all `0` and `#` after the decimal — count the + // total digit-placeholder count to determine fraction width. + return (fractional.match(/[0#]/g) ?? []).length; +} + +/** + * Humanize a column identifier into a Title-Case display string. + * + * Handles snake_case, camelCase, PascalCase, SCREAMING_SNAKE_CASE, and + * already-spaced inputs. Sanitizes non-identifier characters (the same + * pattern as the existing `formatFieldLabel`'s safe-key regex) so user- + * supplied names cannot inject markup. + */ +function humanizeIdentifier(name: string): string { + const safe = name.replace(/[^a-zA-Z0-9_\- ]/g, ""); + if (safe.length === 0) return ""; + + // Insert a space before capitals (camelCase / PascalCase boundaries), + // replace `_` and `-` with spaces, collapse runs, then title-case each word. + const withSpaces = safe + .replace(/([a-z0-9])([A-Z])/g, "$1 $2") + .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2") + .replace(/[_-]+/g, " ") + .replace(/\s+/g, " ") + .trim(); + + if (withSpaces.length === 0) return ""; + + return withSpaces + .split(" ") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(" "); +} diff --git a/packages/appkit-ui/src/format/index.ts b/packages/appkit-ui/src/format/index.ts new file mode 100644 index 000000000..fabc10ac8 --- /dev/null +++ b/packages/appkit-ui/src/format/index.ts @@ -0,0 +1,32 @@ +/** + * Library-agnostic format utilities for UC Metric View consumption. + * + * Phase 5 of `prd/analytics-metric-view-source.md` ships these so customers + * can wire metric metadata into Plotly / ECharts / table cells / KPI tiles + * without an AppKit-specific chart-prop lock-in. + * + * Three primary functions: + * - {@link formatValue} — turns a raw value + format spec into a display string. + * - {@link formatLabel} — returns `display_name` from metadata, falls back to humanized column name. + * - {@link toD3Format} — converts UC printf-style specs to d3-format strings. + * + * Plus a registration API for the build-time metadata bundle: + * - {@link registerMetricsMetadata} — call once at app startup with the + * imported `metrics.metadata.json`. + * - {@link getMetricMetadata} — used by `useMetricView` (and any custom + * glue code) to read per-metric metadata back out. + */ + +export { formatLabel, formatValue, toD3Format } from "./format"; +export { + _getRegisteredBundleForTesting, + clearMetricsMetadata, + getMetricMetadata, + registerMetricsMetadata, +} from "./registry"; +export type { + ColumnMetadata, + FormatSpec, + MetricMetadata, + MetricsMetadataBundle, +} from "./types"; diff --git a/packages/appkit-ui/src/format/registry.ts b/packages/appkit-ui/src/format/registry.ts new file mode 100644 index 000000000..33c0827c8 --- /dev/null +++ b/packages/appkit-ui/src/format/registry.ts @@ -0,0 +1,78 @@ +import type { MetricMetadata, MetricsMetadataBundle } from "./types"; + +/** + * In-memory store for the build-time-bundled metric semantic metadata. + * + * The `metrics.metadata.json` artifact emitted by the AppKit type-generator + * is opt-in: the consuming app imports it and calls + * {@link registerMetricsMetadata} once at startup (typically in the same + * module that mounts the React tree). The `useMetricView` hook reads from + * this store on every render via {@link getMetricMetadata}; the returned + * object reference is stable across re-renders for the same metric key. + * + * The store is process-global by design — the metadata is inert data + * (display names, format specs, descriptions) and there is no per-user or + * per-request variation. Using a module-level singleton keeps the surface + * minimal: the customer touches this once, the hook reads it many times. + */ +let registeredBundle: MetricsMetadataBundle | null = null; + +/** + * Register the build-time semantic-metadata bundle for the running app. + * + * Typical usage at app startup: + * + * ```ts + * import metricsMetadata from "../shared/appkit-types/metrics.metadata.json"; + * import { registerMetricsMetadata } from "@databricks/appkit-ui/format"; + * + * registerMetricsMetadata(metricsMetadata); + * ``` + * + * Calling this multiple times replaces the previous bundle — useful in dev + * mode if the type-generator regenerates the file mid-session, but the hook + * is intentionally not reactive to bundle changes (the metadata is + * build-time-frozen at deploy by the PRD's contract). Tests reset between + * runs via {@link clearMetricsMetadata}. + */ +export function registerMetricsMetadata( + bundle: MetricsMetadataBundle | null, +): void { + registeredBundle = bundle ?? null; +} + +/** + * Retrieve the metadata for one registered metric. + * + * Returns `null` when: + * - no bundle has been registered (the app didn't opt into the metadata flow), or + * - the bundle has no entry for `metricKey` (typo / out-of-sync registration). + * + * The returned object is a direct reference into the registered bundle — + * {@link useMetricView} relies on this for stable identity across re-renders. + * Callers must not mutate it. + */ +export function getMetricMetadata(metricKey: string): MetricMetadata | null { + if (registeredBundle == null) return null; + const entry = registeredBundle[metricKey]; + return entry ?? null; +} + +/** + * Test-only seam: reset the registry between tests so leftover state from a + * previous test cannot affect the next one. Production code never calls this. + * + * @internal + */ +export function clearMetricsMetadata(): void { + registeredBundle = null; +} + +/** + * Test-only seam: introspect the registered bundle. + * + * @internal + */ +export function _getRegisteredBundleForTesting(): MetricsMetadataBundle | null { + return registeredBundle; +} diff --git a/packages/appkit-ui/src/format/types.ts b/packages/appkit-ui/src/format/types.ts new file mode 100644 index 000000000..809c12b1c --- /dev/null +++ b/packages/appkit-ui/src/format/types.ts @@ -0,0 +1,71 @@ +/** + * Library-agnostic semantic-metadata types used by the format utilities and + * the `useMetricView` hook's `metadata` return field. + * + * Lives in `@databricks/appkit-ui/format` — no React dependency, no SSE + * dependency. The shape mirrors the build-time `metrics.metadata.json` + * artifact one-for-one so consumers can typecheck against the file they + * imported without an extra cast. + * + * Source of truth: the YAML 1.1 metric-view spec on Unity Catalog. Every + * field except `type` is optional in the YAML, so every consumer is required + * to defend against absence (the format utilities all have sensible + * fallbacks; the hook returns `null` when the bundle has not been registered). + */ + +/** + * Printf-style format spec sourced from a YAML 1.1 metric view. The framework + * forwards the verbatim string — we deliberately do not invent a format DSL. + * + * Examples (from the UC metric-view docs): + * - `"$#,##0.00"` — currency with two decimals (`"$1,234.56"`) + * - `"0.0%"` — percentage with one decimal (`"42.7%"`) + * - `"#,##0"` — integer with thousands separator (`"1,234"`) + * - `"0.000"` — fixed-precision number (`"1.235"`) + * + * Unrecognized specs fall back to localized number formatting (`formatValue`) + * or identity (`toD3Format`). + */ +export type FormatSpec = string; + +/** + * Per-column metadata as emitted into the build-time bundle and returned by + * the hook. Mirrors {@link MetricColumnMetadata} in + * `@databricks/appkit-ui/react` — duplicated here because format utilities + * must not import from the React subpath. + */ +export interface ColumnMetadata { + /** Databricks SQL type ("STRING", "DECIMAL(38,2)", "TIMESTAMP", ...). */ + type: string; + /** YAML 1.1 `display_name` — used by `formatLabel` as the canonical title. */ + display_name?: string; + /** YAML 1.1 `format` spec — printf-style passthrough. */ + format?: FormatSpec; + /** Column-level documentation. */ + description?: string; + /** Allowed time-grains (only present on time-typed dimensions). */ + time_grain?: readonly string[]; +} + +/** + * One metric's complete semantic-metadata bundle. + * + * Top-level matches the shape in the build-time `metrics.metadata.json` file: + * `Record`. Each entry carries the FQN, the + * execution lane, and per-column metadata for measures and dimensions. + */ +export interface MetricMetadata { + source: string; + lane: "sp" | "obo"; + measures: Record; + dimensions: Record; +} + +/** + * The full registered metadata bundle. + * + * Top-level keys are metric keys; each entry is a {@link MetricMetadata} for + * one metric. Pass the imported JSON to `registerMetricsMetadata()` once at + * app startup; the hook reads it back on every render via a stable lookup. + */ +export type MetricsMetadataBundle = Record; diff --git a/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view-metadata.test.ts b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view-metadata.test.ts new file mode 100644 index 000000000..c75bdb225 --- /dev/null +++ b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view-metadata.test.ts @@ -0,0 +1,213 @@ +import { renderHook } from "@testing-library/react"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { + clearMetricsMetadata, + type MetricsMetadataBundle, + registerMetricsMetadata, +} from "@/format"; + +// Mock connectSSE so the hook's render path doesn't fire a real network call +// (we don't need the data flow here — just metadata reading). +vi.mock("@/js", () => ({ + connectSSE: vi.fn().mockImplementation( + () => + new Promise((resolve) => { + setTimeout(resolve, 0); + }), + ), +})); + +import { useMetricView } from "../use-metric-view"; + +const REVENUE_BUNDLE: MetricsMetadataBundle = { + revenue: { + source: "appkit_demo.public.revenue_metrics", + lane: "sp", + measures: { + arr: { + type: "DECIMAL(38,2)", + display_name: "Annual Recurring Revenue", + format: "$#,##0.00", + }, + mrr: { + type: "DECIMAL(38,2)", + display_name: "Monthly Recurring Revenue", + format: "$#,##0.00", + }, + }, + dimensions: { + region: { type: "STRING", display_name: "Region" }, + created_at: { + type: "TIMESTAMP", + display_name: "Period", + time_grain: ["day", "week", "month"], + }, + }, + }, + other_metric: { + source: "demo.public.other", + lane: "sp", + measures: { count: { type: "BIGINT" } }, + dimensions: {}, + }, +}; + +describe("useMetricView — Phase 5 metadata return field", () => { + afterEach(() => { + clearMetricsMetadata(); + vi.clearAllMocks(); + }); + + test("metadata is null when no bundle has been registered", () => { + const { result } = renderHook(() => + useMetricView("revenue", { measures: ["arr"] }), + ); + expect(result.current.metadata).toBeNull(); + }); + + test("metadata returns the per-metric subset when registered", () => { + registerMetricsMetadata(REVENUE_BUNDLE); + const { result } = renderHook(() => + useMetricView("revenue", { measures: ["arr"] }), + ); + expect(result.current.metadata).not.toBeNull(); + expect(result.current.metadata?.measures.arr.format).toBe("$#,##0.00"); + expect(result.current.metadata?.measures.arr.display_name).toBe( + "Annual Recurring Revenue", + ); + }); + + test("metadata excludes other metrics in the same bundle", () => { + registerMetricsMetadata(REVENUE_BUNDLE); + const { result } = renderHook(() => + useMetricView("revenue", { measures: ["arr"] }), + ); + const meta = result.current.metadata; + expect(meta).not.toBeNull(); + // The metadata is the revenue entry only — `other_metric` is not nested in. + expect(Object.keys(meta?.measures ?? {})).toEqual(["arr", "mrr"]); + // No "other_metric" key leaks through. + expect( + (meta as unknown as Record).other_metric, + ).toBeUndefined(); + }); + + test("metadata is available immediately on first render (before data resolves)", () => { + registerMetricsMetadata(REVENUE_BUNDLE); + const { result } = renderHook(() => + useMetricView("revenue", { measures: ["arr"] }), + ); + // PRD contract: metadata is build-time-bundled, not fetched, so it's + // available even when the data is still loading. + expect(result.current.data).toBeNull(); + expect(result.current.loading).toBe(true); + expect(result.current.metadata).not.toBeNull(); + }); + + test("metadata is stable across re-renders for the same metric key", () => { + registerMetricsMetadata(REVENUE_BUNDLE); + const { result, rerender } = renderHook( + ({ measures }) => + useMetricView("revenue", { measures } as { measures: ["arr" | "mrr"] }), + { + initialProps: { measures: ["arr"] as ["arr" | "mrr"] }, + }, + ); + + const firstRef = result.current.metadata; + rerender({ measures: ["mrr"] }); + const secondRef = result.current.metadata; + rerender({ measures: ["arr"] }); + const thirdRef = result.current.metadata; + + // Same metric key → same metadata reference across re-renders, regardless + // of how `args` changes. + expect(firstRef).toBe(secondRef); + expect(firstRef).toBe(thirdRef); + }); + + test("metadata changes when the metric key changes", () => { + registerMetricsMetadata(REVENUE_BUNDLE); + // The cast escapes the cross-file MetricRegistry augmentation that the + // sibling type-tests file declares — those augmentations leak into the + // global type universe of the test project, but we want this hook test to + // exercise the runtime metadata-resolution logic with synthetic keys. + const { result, rerender } = renderHook( + ({ key }: { key: string }) => + useMetricView(key as never, { measures: ["count"] } as never), + { + initialProps: { key: "revenue" }, + }, + ); + + const revenueMetadata = result.current.metadata as unknown as { + source: string; + } | null; + rerender({ key: "other_metric" }); + const otherMetadata = result.current.metadata as unknown as { + source: string; + } | null; + + expect(revenueMetadata).not.toBe(otherMetadata); + expect(revenueMetadata?.source).toBe("appkit_demo.public.revenue_metrics"); + expect(otherMetadata?.source).toBe("demo.public.other"); + }); + + test("metadata is null when the metric key is not in the registered bundle", () => { + registerMetricsMetadata(REVENUE_BUNDLE); + const { result } = renderHook(() => + // Deliberate test of runtime fallback when the metric key is missing + // from the registered bundle. The cast escapes the augmented-registry + // type narrowing — the runtime semantics are what matter here. + useMetricView("not_in_bundle" as never, { measures: ["x"] } as never), + ); + expect(result.current.metadata).toBeNull(); + }); + + test("metadata exposes time_grain on time-typed dimensions", () => { + registerMetricsMetadata(REVENUE_BUNDLE); + const { result } = renderHook(() => + useMetricView("revenue", { measures: ["arr"] }), + ); + // The cross-file MetricRegistry augmentation narrows the dimensions + // shape, so we read it back as the structural metadata type to inspect + // runtime values. + const dims = (result.current.metadata?.dimensions ?? {}) as Record< + string, + { time_grain?: readonly string[] } + >; + expect(dims.created_at?.time_grain).toEqual(["day", "week", "month"]); + expect(dims.region?.time_grain).toBeUndefined(); + }); + + test("metadata reference is stable when bundle is re-registered with the same metric key (PRD's stable-not-reactive contract)", () => { + registerMetricsMetadata(REVENUE_BUNDLE); + const { result, rerender } = renderHook(() => + useMetricView("revenue", { measures: ["arr"] }), + ); + const firstRef = result.current.metadata; + expect(firstRef).not.toBeNull(); + + // Re-register a new bundle with the same key but different data. The + // hook is intentionally NOT reactive to bundle changes — the PRD says + // metadata is build-time-frozen and stable for the lifetime of a metric + // key. Re-registration during a session is a dev-mode hot-reload signal + // that requires a remount to pick up; mid-render swaps would break the + // "stable across re-renders" contract that downstream memoization + // depends on. + const newBundle: MetricsMetadataBundle = { + revenue: { + source: "demo.public.new_revenue", + lane: "sp", + measures: { arr: { type: "DECIMAL", format: "0.00" } }, + dimensions: {}, + }, + }; + registerMetricsMetadata(newBundle); + rerender(); + const refAfterRegister = result.current.metadata; + // Same reference — useMemo keys on metricKey, so within a single mount + // the hook returns the originally-resolved metadata until a remount. + expect(refAfterRegister).toBe(firstRef); + }); +}); diff --git a/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view-types.test.ts b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view-types.test.ts index edd061cec..30271c44d 100644 --- a/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view-types.test.ts +++ b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view-types.test.ts @@ -4,9 +4,12 @@ import type { Filter, MeasureKey, MetricFilterOperator, + MetricMetadata, + MetricSemanticMetadata, Predicate, TimeGrain, UseMetricViewArgs, + UseMetricViewResult, UseMetricViewRow, } from "../types"; @@ -34,6 +37,24 @@ declare module "../types" { measureKeys: "arr" | "mrr"; dimensionKeys: "region" | "segment" | "created_at"; timeGrains: "day" | "month" | "week"; + metadata: { + measures: { + arr: { + type: "DECIMAL(38,2)"; + display_name: "Annual Recurring Revenue"; + format: "$#,##0.00"; + }; + mrr: { type: "DECIMAL(38,2)" }; + }; + dimensions: { + region: { type: "STRING" }; + segment: { type: "STRING" }; + created_at: { + type: "TIMESTAMP"; + time_grain: readonly ["day", "month", "week"]; + }; + }; + }; }; flat_metric: { key: "flat_metric"; @@ -44,6 +65,10 @@ declare module "../types" { measureKeys: "count"; dimensionKeys: never; timeGrains: never; + metadata: { + measures: { count: { type: "BIGINT" } }; + dimensions: Record; + }; }; } } @@ -221,3 +246,48 @@ describe("Filter / Predicate — recursive shape and registry narrowing", expectTypeOf().toEqualTypeOf(); }); }); + +// ── Phase 5: MetricMetadata narrows per-metric, hook return shape carries metadata ── +describe("MetricMetadata — Phase 5 metadata narrowing", () => { + test("MetricMetadata narrows to the registry's metadata shape for registered keys", () => { + type Meta = MetricMetadata<"revenue">; + expectTypeOf< + Meta["measures"]["arr"]["format"] + >().toEqualTypeOf<"$#,##0.00">(); + expectTypeOf< + Meta["measures"]["arr"]["display_name"] + >().toEqualTypeOf<"Annual Recurring Revenue">(); + }); + + test("MetricMetadata exposes time_grain literal tuple on time-typed dims", () => { + type Meta = MetricMetadata<"revenue">; + expectTypeOf< + Meta["dimensions"]["created_at"]["time_grain"] + >().toEqualTypeOf(); + }); + + test("MetricMetadata's measures only contain the metric's own keys (not other metrics')", () => { + type Meta = MetricMetadata<"revenue">; + type MeasureKeys = keyof Meta["measures"]; + expectTypeOf().toEqualTypeOf<"arr" | "mrr">(); + + type FlatMeta = MetricMetadata<"flat_metric">; + type FlatKeys = keyof FlatMeta["measures"]; + expectTypeOf().toEqualTypeOf<"count">(); + }); + + test("MetricMetadata falls back to the structural shape for unregistered keys", () => { + type Meta = MetricMetadata; + expectTypeOf().toEqualTypeOf(); + }); + + test("UseMetricViewResult carries metadata typed per K", () => { + type Result = UseMetricViewResult< + { arr: number }, + MetricMetadata<"revenue"> + >; + type MetaField = Result["metadata"]; + // metadata is the metric's literal-typed metadata or null. + expectTypeOf().toEqualTypeOf | null>(); + }); +}); diff --git a/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts index fb2cce7e3..1cc29bbf4 100644 --- a/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts +++ b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts @@ -1,5 +1,6 @@ import { act, renderHook, waitFor } from "@testing-library/react"; import { afterEach, describe, expect, test, vi } from "vitest"; +import { clearMetricsMetadata } from "@/format"; // Mock connectSSE — capture callbacks so we can simulate SSE events. let capturedCallbacks: { @@ -29,6 +30,7 @@ describe("useMetricView", () => { afterEach(() => { capturedCallbacks = {}; vi.clearAllMocks(); + clearMetricsMetadata(); }); test("initial state is loading=true with autoStart (default)", () => { @@ -41,6 +43,8 @@ describe("useMetricView", () => { // loading flips to true before the test inspects state. expect(result.current.loading).toBe(true); expect(result.current.error).toBeNull(); + // Phase 5: metadata is null when no bundle has been registered. + expect(result.current.metadata).toBeNull(); }); test("connects to /api/analytics/metric/ with the request payload", () => { diff --git a/packages/appkit-ui/src/react/hooks/index.ts b/packages/appkit-ui/src/react/hooks/index.ts index 32998a091..7e586855b 100644 --- a/packages/appkit-ui/src/react/hooks/index.ts +++ b/packages/appkit-ui/src/react/hooks/index.ts @@ -8,10 +8,13 @@ export type { InferServingRequest, InferServingResponse, MeasureKey, + MetricColumnMetadata, MetricFilterOperator, MetricKey, + MetricMetadata, MetricRegistry, MetricRow, + MetricSemanticMetadata, PluginRegistry, Predicate, QueryRegistry, diff --git a/packages/appkit-ui/src/react/hooks/types.ts b/packages/appkit-ui/src/react/hooks/types.ts index 4a47dd2d5..9111c268c 100644 --- a/packages/appkit-ui/src/react/hooks/types.ts +++ b/packages/appkit-ui/src/react/hooks/types.ts @@ -149,8 +149,10 @@ export interface ServingClientConfig { * AppKit type-generator (parallel to {@link QueryRegistry}). * * Each registered metric key contributes an entry whose shape carries the - * FQN, lane, and the structured measure / dimension lists harvested from the - * build-time DESCRIBE TABLE EXTENDED ... AS JSON call. + * FQN, lane, the structured measure / dimension lists harvested from the + * build-time DESCRIBE TABLE EXTENDED ... AS JSON call, and (Phase 5) the + * per-column semantic metadata bundle (display name, format spec, + * description, time-grain options). * * @example * ```ts @@ -165,6 +167,16 @@ export interface ServingClientConfig { * measureKeys: "arr" | "mrr"; * dimensionKeys: "region" | "created_at"; * timeGrains: "day" | "week" | "month"; + * metadata: { + * measures: { + * arr: { type: "DECIMAL(38,2)"; display_name: "Annual Recurring Revenue"; format: "$#,##0.00" }; + * mrr: { type: "DECIMAL(38,2)" }; + * }; + * dimensions: { + * region: { type: "STRING" }; + * created_at: { type: "TIMESTAMP"; time_grain: readonly ["day", "week", "month"] }; + * }; + * }; * }; * } * } @@ -231,6 +243,73 @@ type MetricDimensionMap = K extends AugmentedRegistry /** Full result row type for a registered metric (measures + dimensions). */ export type MetricRow = MetricMeasureMap & MetricDimensionMap; +// ============================================================================ +// Metric View Semantic Metadata (Phase 5 — display names, format specs, ...) +// ============================================================================ + +/** + * The per-column semantic-metadata shape exposed via `useMetricView`'s + * `metadata` return field and `formatLabel` / `formatValue`'s second argument. + * + * Mirrors the `metrics.metadata.json` build-time artifact one-for-one. Every + * field except `type` is optional — the YAML 1.1 metric view spec marks them + * as opt-in, so the consumer's chart code defends against absence (e.g. + * `formatLabel` falls back to camelCase humanization when `display_name` is + * absent). + */ +export interface MetricColumnMetadata { + /** Databricks SQL type ("STRING", "DECIMAL(38,2)", "TIMESTAMP", ...). */ + type: string; + /** YAML 1.1 `display_name` — used by `formatLabel` as the canonical title. */ + display_name?: string; + /** + * YAML 1.1 `format` — printf-style spec (`"$#,##0.00"`, `"0.0%"`, etc). + * Consumed by `formatValue` (returns formatted string) and `toD3Format` + * (returns d3-format-compatible string for Plotly's `tickformat` / + * ECharts' `valueFormatter`). + */ + format?: string; + /** Column-level documentation, surfaced in tooltips by chart components. */ + description?: string; + /** + * Allowed time-grains (only present on time-typed dimensions). Phase 2 + * widening: lets the call-site narrow `timeGrain` to the dim's allowed list. + */ + time_grain?: readonly string[]; +} + +/** + * One metric's complete semantic-metadata bundle. + * + * Mirrors the entry shape inside `metrics.metadata.json`. Returned verbatim + * by `useMetricView` in its `metadata` field — TypeScript narrows + * `metadata.measures.` and `metadata.dimensions.` from the + * registry's per-metric `metadata` augmentation when `K` is a registered key. + */ +export interface MetricSemanticMetadata { + source: string; + lane: "sp" | "obo"; + measures: Record; + dimensions: Record; +} + +/** + * Type-narrowed metadata for a registered metric `K`. + * + * When `K` is a registered key, resolves to the registry's `metadata` shape + * (per-column literal-typed `display_name` / `format` / `time_grain`). When + * `K` is `string` (no augmentation), resolves to the structural + * {@link MetricSemanticMetadata}. + * + * Consumers usually destructure: `metadata.measures.arr.format`, + * `metadata.dimensions.created_at.time_grain`, etc. + */ +export type MetricMetadata = K extends AugmentedRegistry + ? MetricRegistry[K] extends { metadata: infer Meta } + ? Meta + : MetricSemanticMetadata + : MetricSemanticMetadata; + // ============================================================================ // Filter Specification (Phase 3 — recursive AND/OR with 12 v1 operators) // ============================================================================ @@ -386,9 +465,24 @@ export interface UseMetricViewOptions { maxParametersSize?: number; } -/** Phase 2 result shape: { data, loading, error }. */ -export interface UseMetricViewResult { +/** + * Phase 5 result shape: `{ data, metadata, loading, error }`. + * + * `metadata` is the build-time-bundled semantic metadata for the queried + * metric (measures + dimensions only — not other metrics in the registry). + * It is available **before** the data loads (it comes from the build-time + * artifact, not the network) and is stable across re-renders for the same + * metric key (the runtime registry returns the same object reference). + * + * When the consuming app has not registered the metadata bundle (via + * `registerMetricsMetadata` in `@databricks/appkit-ui/format`), `metadata` + * resolves to `null`. The PRD's contract is "available even when data is + * null" — but the bundle itself is opt-in, so a `null` here is the + * unregistered-app signal. + */ +export interface UseMetricViewResult { data: TRow[] | null; + metadata: TMetadata | null; loading: boolean; error: string | null; } diff --git a/packages/appkit-ui/src/react/hooks/use-metric-view.ts b/packages/appkit-ui/src/react/hooks/use-metric-view.ts index de19ae666..b4216a417 100644 --- a/packages/appkit-ui/src/react/hooks/use-metric-view.ts +++ b/packages/appkit-ui/src/react/hooks/use-metric-view.ts @@ -1,10 +1,12 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { getMetricMetadata } from "@/format"; import { connectSSE } from "@/js"; import type { AnalyticsFormat, DimensionKey, MeasureKey, MetricKey, + MetricMetadata, UseMetricViewArgs, UseMetricViewOptions, UseMetricViewResult, @@ -14,23 +16,30 @@ import type { /** * Subscribe to a metric-view query over SSE. * - * Phase 2 surface — accepts `{ measures, dimensions?, timeGrain?, limit? }`. + * Phase 5 surface — accepts `{ measures, dimensions?, timeGrain?, filter?, limit? }`. * The result row type narrows at the call site to * `Pick, M[number] | D[number]>` based on the chosen measures * and dimensions, so chart code receives the exact shape it asked for. * + * Returns `{ data, metadata, loading, error }`. The `metadata` field carries + * the build-time-bundled semantic metadata for the queried metric (display + * names, format specs, descriptions). `metadata` is available **before** the + * data loads and is stable across re-renders for the same metric key. + * * Use `as const` on the `measures` and `dimensions` arrays at the call site * to preserve literal types (the same pattern used elsewhere in AppKit for * registry-narrowed APIs). * * @example * ```tsx - * const { data, loading, error } = useMetricView("revenue", { + * const { data, metadata, loading, error } = useMetricView("revenue", { * measures: ["arr"] as const, * dimensions: ["region", "created_at"] as const, * timeGrain: "month", * }); * // data: Array<{ arr: number; region: string; created_at: string }> | null + * // metadata.measures.arr.format → "$#,##0.00" + * // metadata.measures.arr.display_name → "Annual Recurring Revenue" * ``` */ export function useMetricView< @@ -44,7 +53,7 @@ export function useMetricView< metricKey: K, args: UseMetricViewArgs, options: UseMetricViewOptions = {} as UseMetricViewOptions, -): UseMetricViewResult> { +): UseMetricViewResult, MetricMetadata> { if (!metricKey || metricKey.trim().length === 0) { throw new Error("useMetricView: 'metricKey' must be a non-empty string."); } @@ -61,6 +70,18 @@ export function useMetricView< const [error, setError] = useState(null); const abortControllerRef = useRef(null); + // Read the build-time semantic-metadata bundle from the format registry. + // The lookup is keyed only by `metricKey`, so the returned reference is + // stable across re-renders for the same metric (the PRD's contract: + // "metadata is stable, not reactive"). Memoizing here is also defense- + // in-depth — even if a customer hot-reloads the metadata bundle, this hook + // still returns the same object reference for the lifetime of the render + // cycle. + const metadata = useMemo( + () => getMetricMetadata(metricKey) as MetricMetadata | null, + [metricKey], + ); + const payload = useMemo(() => { try { const dimensions = args.dimensions ? [...args.dimensions] : undefined; @@ -195,5 +216,5 @@ export function useMetricView< }; }, [start, autoStart]); - return { data, loading, error }; + return { data, metadata, loading, error }; } diff --git a/packages/appkit-ui/tsdown.config.ts b/packages/appkit-ui/tsdown.config.ts index f55a51457..50f2f8a49 100644 --- a/packages/appkit-ui/tsdown.config.ts +++ b/packages/appkit-ui/tsdown.config.ts @@ -9,6 +9,7 @@ export default defineConfig([ "src/js/beta.ts", "src/react/index.ts", "src/react/beta.ts", + "src/format/index.ts", ], outDir: "dist", platform: "browser", diff --git a/packages/appkit/src/type-generator/index.ts b/packages/appkit/src/type-generator/index.ts index 0b700dc10..6491e7bb8 100644 --- a/packages/appkit/src/type-generator/index.ts +++ b/packages/appkit/src/type-generator/index.ts @@ -5,6 +5,7 @@ import { createLogger } from "../logging/logger"; import { createWorkspaceDescribeFetcher, type DescribeFetcher, + generateMetricsMetadataJson, generateMetricTypeDeclarations, type MetricSchema, readMetricConfig, @@ -62,6 +63,10 @@ declare module "@databricks/appkit-ui/react" { * @param options.metricOutFile - optional output file for the MetricRegistry * augmentation. Defaults to a sibling `metric.d.ts` file under the same * directory as `outFile`. Skipped entirely if `metric.json` is absent. + * @param options.metricMetadataOutFile - optional output file for the + * build-time semantic metadata JSON bundle (`metrics.metadata.json`). + * Defaults to a sibling of `metricOutFile`. Skipped entirely if + * `metric.json` is absent. * @param options.metricFetcher - optional DescribeFetcher used by * {@link syncMetrics}. Tests inject a mock; production builds let the * default WorkspaceClient-backed fetcher be created lazily. @@ -72,6 +77,7 @@ export async function generateFromEntryPoint(options: { warehouseId: string; noCache?: boolean; metricOutFile?: string; + metricMetadataOutFile?: string; metricFetcher?: DescribeFetcher; }) { const { @@ -80,6 +86,7 @@ export async function generateFromEntryPoint(options: { warehouseId, noCache, metricOutFile, + metricMetadataOutFile, metricFetcher, } = options; const projectRoot = resolveProjectRoot(outFile); @@ -134,8 +141,19 @@ export async function generateFromEntryPoint(options: { const metricDeclarations = generateMetricTypeDeclarations(metricSchemas); await fs.mkdir(path.dirname(metricFile), { recursive: true }); await fs.writeFile(metricFile, metricDeclarations, "utf-8"); + + // Phase 5: emit the semantic-metadata JSON bundle alongside the .d.ts. + // The hook imports this artifact (via a registration call from the + // consuming app) and exposes the per-metric subset on its return value. + const metadataFile = + metricMetadataOutFile ?? + path.join(path.dirname(metricFile), METRIC_METADATA_FILE); + const metadataJson = generateMetricsMetadataJson(metricSchemas); + await fs.mkdir(path.dirname(metadataFile), { recursive: true }); + await fs.writeFile(metadataFile, metadataJson, "utf-8"); + logger.debug( - "Wrote MetricRegistry augmentation for %d metric(s)", + "Wrote MetricRegistry augmentation + metadata bundle for %d metric(s)", metricSchemas.length, ); } @@ -161,3 +179,15 @@ export const ANALYTICS_TYPES_FILE = "analytics.d.ts"; export const SERVING_TYPES_FILE = "serving.d.ts"; /** Default filename for metric-view registry type declarations. */ export const METRIC_TYPES_FILE = "metric.d.ts"; +/** + * Default filename for the build-time semantic-metadata JSON bundle. + * + * Sibling of {@link METRIC_TYPES_FILE}. The JSON shape is + * `Record` — see + * `MetricsMetadataBundle` in `metric-registry.ts`. The consuming app imports + * this file at build time (via Vite's JSON loader / Webpack's `import` etc.) + * and registers it through `@databricks/appkit-ui/format`'s + * `registerMetricsMetadata()` so the React hook can return per-metric + * `metadata` without a second network round-trip. + */ +export const METRIC_METADATA_FILE = "metrics.metadata.json"; diff --git a/packages/appkit/src/type-generator/metric-registry.ts b/packages/appkit/src/type-generator/metric-registry.ts index 2a5e8cad1..6813a9e05 100644 --- a/packages/appkit/src/type-generator/metric-registry.ts +++ b/packages/appkit/src/type-generator/metric-registry.ts @@ -58,6 +58,13 @@ interface ResolvedMetricEntry { * Phase 1 captured measure flags + types. Phase 2 widens to time-typed * dimensions: a column is "time-typed" iff its DESCRIBE entry carries a * non-empty `time_grain` attribute listing the allowed grains for that column. + * + * Phase 5 captures the YAML 1.1 semantic-metadata fields so the build-time + * artifact is a complete record of what the metric view declares: display name + * (used by `formatLabel` to render axis titles / legend entries / tooltips), + * format spec (printf-like string consumed by `formatValue` and `toD3Format`), + * and description (column-level documentation). All three are optional in the + * YAML; the extractor leaves the field undefined when absent. */ export interface MetricColumnMetadata { name: string; @@ -66,6 +73,20 @@ export interface MetricColumnMetadata { isMeasure: boolean; /** Optional column comment / display description (best-effort). */ description?: string; + /** + * Human-readable display name from the YAML 1.1 `display_name` attribute. + * Used by `formatLabel` as the canonical axis / legend / tooltip text; + * absent → callers fall back to camelCase / snake_case humanization of `name`. + */ + displayName?: string; + /** + * Printf-style format spec from the YAML 1.1 `format` attribute (e.g. + * `"$#,##0.00"`, `"0.0%"`, `"#,##0"`). `formatValue` and `toD3Format` + * consume this passthrough — the framework deliberately does not invent a + * format DSL; we forward the YAML's verbatim string and fall back to + * sensible defaults when the spec is absent or unrecognized. + */ + format?: string; /** * Allowed time-grains for this column when present in the YAML's `time_grain` * attribute. Undefined means the column is not time-typed. An empty array is @@ -332,6 +353,12 @@ export function extractMetricColumns(parsed: unknown): MetricColumnMetadata[] { ? obj.description : undefined; + const displayName = extractStringFromAny(obj, [ + "display_name", + "displayName", + ]); + const format = extractStringFromAny(obj, ["format", "format_spec"]); + const timeGrains = extractTimeGrains(obj); columns.push({ @@ -339,6 +366,8 @@ export function extractMetricColumns(parsed: unknown): MetricColumnMetadata[] { type, isMeasure, description, + ...(displayName ? { displayName } : {}), + ...(format ? { format } : {}), ...(timeGrains ? { timeGrains } : {}), }); } @@ -346,6 +375,34 @@ export function extractMetricColumns(parsed: unknown): MetricColumnMetadata[] { return columns; } +/** + * Read a non-empty string attribute from a DESCRIBE column entry, tolerating + * the multiple shapes UC has shipped for this metadata over time. + * + * For each candidate name, we check the column object directly, then under + * `metadata.`. The first non-empty trimmed string wins. Empty / missing + * → undefined (the caller leaves the field off the emitted artifact). + */ +function extractStringFromAny( + obj: Record, + candidates: readonly string[], +): string | undefined { + for (const key of candidates) { + const direct = obj[key]; + if (typeof direct === "string" && direct.trim().length > 0) { + return direct; + } + const meta = obj.metadata; + if (meta && typeof meta === "object" && !Array.isArray(meta)) { + const nested = (meta as Record)[key]; + if (typeof nested === "string" && nested.trim().length > 0) { + return nested; + } + } + } + return undefined; +} + /** * Pull the allowed time-grain list for a column from the DESCRIBE entry. * @@ -484,6 +541,9 @@ ${dimensions}; .join(" | ") : "never"; + const measureMetadata = renderMetadataMap(schema.measures, indent); + const dimensionMetadata = renderMetadataMap(schema.dimensions, indent, true); + return ` ${JSON.stringify(schema.key)}: { key: ${JSON.stringify(schema.key)}; source: ${JSON.stringify(schema.source)}; @@ -493,6 +553,61 @@ ${dimensions}; measureKeys: ${measureUnion}; dimensionKeys: ${dimensionUnion}; timeGrains: ${timeGrainUnion}; + metadata: { + measures: ${measureMetadata}; + dimensions: ${dimensionMetadata}; + }; + }`; +} + +/** + * Render the type-level shape of a column's semantic-metadata map for the + * `metadata` field of a MetricRegistry entry. + * + * The shape mirrors {@link MetricColumnSemanticMetadata}: each column emits an + * object literal with `type` (string literal) plus optional `display_name`, + * `format`, `description` (string literals when known, dropped when absent), + * and — for dimensions only — `time_grain` (the column's allowed-grain tuple + * literal). + * + * When the column list is empty, the type collapses to `Record` + * so consumers can still index into `metadata.measures` / `metadata.dimensions` + * without TypeScript errors. + */ +function renderMetadataMap( + cols: MetricColumnMetadata[], + indent: string, + includeTimeGrain = false, +): string { + if (cols.length === 0) return "Record"; + + const inner = cols + .map((col) => { + const fields: string[] = [`type: ${JSON.stringify(col.type)}`]; + if (col.displayName) { + fields.push(`display_name: ${JSON.stringify(col.displayName)}`); + } + if (col.format) { + fields.push(`format: ${JSON.stringify(col.format)}`); + } + if (col.description) { + fields.push(`description: ${JSON.stringify(col.description)}`); + } + if (includeTimeGrain && col.timeGrains && col.timeGrains.length > 0) { + const grainTuple = col.timeGrains + .map((g) => JSON.stringify(g)) + .join(", "); + fields.push(`time_grain: readonly [${grainTuple}]`); + } + const fieldsBlock = fields.map((f) => `${indent} ${f}`).join(";\n"); + return `${indent}${JSON.stringify(col.name)}: { +${fieldsBlock}; +${indent}}`; + }) + .join(";\n"); + + return `{ +${inner}; }`; } @@ -539,6 +654,131 @@ export function generateMetricTypeDeclarations( return metricFileHeader() + renderMetricRegistry(schemas); } +/** + * Per-column metadata as emitted into the build-time JSON artifact. + * + * The shape is deliberately narrow — we forward what the YAML 1.1 declared + * (type, display name, format spec, description) plus the time-grain list for + * dimensions. Consumers (the React hook, the format utilities) destructure + * only the fields they need; absent fields stay absent rather than carrying + * empty-string sentinels so JSON.stringify output is minimal. + * + * Internal — exposed via the {@link buildMetricsMetadataBundle} return shape. + * Library consumers see this shape mirrored verbatim in + * `@databricks/appkit-ui/format`'s `ColumnMetadata` (they import there, not + * here). + */ +interface MetricColumnSemanticMetadata { + type: string; + display_name?: string; + format?: string; + description?: string; + /** Only emitted on dimension entries where the YAML declared a non-empty `time_grain`. */ + time_grain?: readonly string[]; +} + +/** + * One metric's complete semantic-metadata bundle. + * + * Splits cleanly into measures + dimensions so the consuming hook can return + * the exact subset for the queried metric without scanning the rest of the + * registry. + */ +interface MetricSemanticMetadataEntry { + source: string; + lane: MetricLane; + measures: Record; + dimensions: Record; +} + +/** + * Top-level shape of `metrics.metadata.json` — keyed by metric key. + * + * Loaded by: + * - the server-side `loadMetricRegistry` (for body-validator awareness of + * display names + types in error messages, when wired up in a follow-on) + * - the client-side `useMetricView` hook (returned in the `metadata` field) + * - any chart-library glue code that wants direct access to format specs / + * display names (Plotly tickformat, ECharts valueFormatter, table cells, ...) + */ +type MetricsMetadataBundle = Record; + +/** + * Pure function: turn a list of metric schemas into the JSON metadata bundle. + * + * Deterministic key order: outer object keys are sorted alphabetically; + * measures and dimensions are emitted in the order they appeared in DESCRIBE + * (Phase 1's preserved-from-YAML order), but each per-column object's fields + * follow a fixed declaration order so snapshot diffs are stable. + * + * The output is `JSON.stringify`'d with two-space indentation by the file + * emitter — keeping the data structure pure here lets unit tests assert on the + * structure without parsing. + */ +export function buildMetricsMetadataBundle( + schemas: MetricSchema[], +): MetricsMetadataBundle { + const bundle: MetricsMetadataBundle = {}; + const sortedSchemas = [...schemas].sort((a, b) => a.key.localeCompare(b.key)); + + for (const schema of sortedSchemas) { + const measures: Record = {}; + for (const m of schema.measures) { + measures[m.name] = buildColumnMetadata(m); + } + + const dimensions: Record = {}; + for (const d of schema.dimensions) { + dimensions[d.name] = buildColumnMetadata(d); + } + + bundle[schema.key] = { + source: schema.source, + lane: schema.lane, + measures, + dimensions, + }; + } + + return bundle; +} + +/** + * Render one column's emitted semantic-metadata object. + * + * Field order is fixed (`type`, `display_name`, `format`, `description`, + * `time_grain`) and absent fields are simply not included, so the snapshot + * diff is always minimal — consumers receive only what the YAML declared. + * + * `time_grain` is only emitted on dimensions (the YAML 1.1 spec restricts it + * to dimension columns). Defends against DESCRIBE leaking a stray attribute + * onto a measure. + */ +function buildColumnMetadata( + col: MetricColumnMetadata, +): MetricColumnSemanticMetadata { + const entry: MetricColumnSemanticMetadata = { type: col.type }; + if (col.displayName) entry.display_name = col.displayName; + if (col.format) entry.format = col.format; + if (col.description) entry.description = col.description; + if (!col.isMeasure && col.timeGrains && col.timeGrains.length > 0) { + entry.time_grain = [...col.timeGrains]; + } + return entry; +} + +/** + * Serialize the metadata bundle to a stable, human-readable JSON string. + * + * Uses two-space indentation and a trailing newline so file diffs are clean + * across regenerations; the bundle's own key order is already sorted by + * {@link buildMetricsMetadataBundle}. + */ +export function generateMetricsMetadataJson(schemas: MetricSchema[]): string { + const bundle = buildMetricsMetadataBundle(schemas); + return `${JSON.stringify(bundle, null, 2)}\n`; +} + /** * Optional dependency-injection seam: the function used to fetch DESCRIBE * results for a given FQN. Production wires this through the WorkspaceClient; diff --git a/packages/appkit/src/type-generator/tests/__snapshots__/metric-registry.test.ts.snap b/packages/appkit/src/type-generator/tests/__snapshots__/metric-registry.test.ts.snap index 830cbdffd..2f37a1629 100644 --- a/packages/appkit/src/type-generator/tests/__snapshots__/metric-registry.test.ts.snap +++ b/packages/appkit/src/type-generator/tests/__snapshots__/metric-registry.test.ts.snap @@ -25,6 +25,26 @@ declare module "@databricks/appkit-ui/react" { measureKeys: "arr"; dimensionKeys: "created_at" | "region" | "segment"; timeGrains: "day" | "month" | "week"; + metadata: { + measures: { + "arr": { + type: "DECIMAL(38,2)"; + description: "Annual recurring revenue"; + }; + }; + dimensions: { + "created_at": { + type: "TIMESTAMP"; + time_grain: readonly ["day", "month", "week"]; + }; + "region": { + type: "STRING"; + }; + "segment": { + type: "STRING"; + }; + }; + }; }; } } @@ -56,6 +76,26 @@ declare module "@databricks/appkit-ui/react" { measureKeys: "arr" | "mrr"; dimensionKeys: "region" | "segment"; timeGrains: never; + metadata: { + measures: { + "arr": { + type: "DECIMAL(38,2)"; + description: "Annual recurring revenue"; + }; + "mrr": { + type: "DECIMAL(38,2)"; + description: "Monthly recurring revenue"; + }; + }; + dimensions: { + "region": { + type: "STRING"; + }; + "segment": { + type: "STRING"; + }; + }; + }; }; } } @@ -71,3 +111,59 @@ declare module "@databricks/appkit-ui/react" { } " `; + +exports[`generateMetricsMetadataJson — snapshot > serializes a representative metric view with display_name + format + time_grain 1`] = ` +"{ + "customer_metrics": { + "source": "appkit_demo.public.customer_metrics", + "lane": "obo", + "measures": { + "churn_rate": { + "type": "DOUBLE", + "display_name": "Churn Rate", + "format": "0.0%" + } + }, + "dimensions": { + "csm_email": { + "type": "STRING", + "display_name": "CSM Email" + } + } + }, + "revenue": { + "source": "appkit_demo.public.revenue_metrics", + "lane": "sp", + "measures": { + "arr": { + "type": "DECIMAL(38,2)", + "display_name": "Annual Recurring Revenue", + "format": "$#,##0.00", + "description": "ARR per quarter" + }, + "growth_rate": { + "type": "DOUBLE", + "display_name": "Growth Rate", + "format": "0.0%" + } + }, + "dimensions": { + "region": { + "type": "STRING", + "display_name": "Region" + }, + "created_at": { + "type": "TIMESTAMP", + "display_name": "Period", + "time_grain": [ + "day", + "month", + "quarter", + "week" + ] + } + } + } +} +" +`; diff --git a/packages/appkit/src/type-generator/tests/metric-registry.test.ts b/packages/appkit/src/type-generator/tests/metric-registry.test.ts index a3e7c697b..9e5514376 100644 --- a/packages/appkit/src/type-generator/tests/metric-registry.test.ts +++ b/packages/appkit/src/type-generator/tests/metric-registry.test.ts @@ -3,7 +3,9 @@ import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { + buildMetricsMetadataBundle, extractMetricColumns, + generateMetricsMetadataJson, generateMetricTypeDeclarations, parseDescribeTableExtendedJson, readMetricConfig, @@ -398,6 +400,346 @@ describe("generateMetricTypeDeclarations — snapshot", () => { }); }); +// ── Phase 5: semantic-metadata extraction (display_name + format) ───────── +describe("extractMetricColumns — Phase 5 semantic metadata", () => { + test("captures display_name from a measure column", () => { + const cols = extractMetricColumns({ + columns: [ + { + name: "arr", + type: "DECIMAL(38,2)", + is_measure: true, + display_name: "Annual Recurring Revenue", + comment: "ARR for the period", + }, + ], + }); + expect(cols[0]).toMatchObject({ + name: "arr", + type: "DECIMAL(38,2)", + isMeasure: true, + displayName: "Annual Recurring Revenue", + description: "ARR for the period", + }); + }); + + test("captures format spec from a measure column", () => { + const cols = extractMetricColumns({ + columns: [ + { + name: "arr", + type: "DECIMAL(38,2)", + is_measure: true, + format: "$#,##0.00", + }, + ], + }); + expect(cols[0].format).toBe("$#,##0.00"); + }); + + test("captures display_name + format on a dimension column", () => { + const cols = extractMetricColumns({ + columns: [ + { + name: "region", + type: "STRING", + is_measure: false, + display_name: "Region", + format: undefined, + }, + ], + }); + expect(cols[0]).toMatchObject({ + name: "region", + isMeasure: false, + displayName: "Region", + }); + expect(cols[0].format).toBeUndefined(); + }); + + test("falls back to displayName camelCase variant", () => { + const cols = extractMetricColumns({ + columns: [ + { + name: "mrr", + type: "DECIMAL", + is_measure: true, + displayName: "Monthly Recurring Revenue", + }, + ], + }); + expect(cols[0].displayName).toBe("Monthly Recurring Revenue"); + }); + + test("reads display_name + format from metadata. (DESCRIBE wrap)", () => { + const cols = extractMetricColumns({ + columns: [ + { + name: "arr", + type: "DECIMAL(38,2)", + metadata: { + is_measure: true, + display_name: "ARR", + format: "$#,##0.00", + }, + }, + ], + }); + expect(cols[0]).toMatchObject({ + isMeasure: true, + displayName: "ARR", + format: "$#,##0.00", + }); + }); + + test("treats empty / whitespace display_name as absent", () => { + const cols = extractMetricColumns({ + columns: [ + { + name: "arr", + type: "DECIMAL", + is_measure: true, + display_name: " ", + format: "", + }, + ], + }); + expect(cols[0].displayName).toBeUndefined(); + expect(cols[0].format).toBeUndefined(); + }); + + test("captures format from format_spec alias", () => { + const cols = extractMetricColumns({ + columns: [ + { + name: "arr", + type: "DECIMAL", + is_measure: true, + format_spec: "$#,##0.00", + }, + ], + }); + expect(cols[0].format).toBe("$#,##0.00"); + }); +}); + +// ── Phase 5: metadata bundle generation ─────────────────────────────────── +describe("buildMetricsMetadataBundle", () => { + test("emits per-metric measures + dimensions records keyed by name", async () => { + const resolution = resolveMetricConfig({ + sp: { revenue: { source: "appkit_demo.public.revenue_metrics" } }, + }); + + const fetcher = async () => + mockDescribeResponse({ + columns: [ + { + name: "arr", + type: "DECIMAL(38,2)", + is_measure: true, + display_name: "Annual Recurring Revenue", + format: "$#,##0.00", + comment: "ARR for the period", + }, + { name: "region", type: "STRING", is_measure: false }, + { + name: "created_at", + type: "TIMESTAMP", + is_measure: false, + time_grain: ["day", "month"], + }, + ], + }); + + const schemas = await syncMetrics(resolution, fetcher); + const bundle = buildMetricsMetadataBundle(schemas); + + expect(bundle.revenue).toMatchObject({ + source: "appkit_demo.public.revenue_metrics", + lane: "sp", + measures: { + arr: { + type: "DECIMAL(38,2)", + display_name: "Annual Recurring Revenue", + format: "$#,##0.00", + description: "ARR for the period", + }, + }, + dimensions: { + region: { + type: "STRING", + }, + created_at: { + type: "TIMESTAMP", + time_grain: ["day", "month"], + }, + }, + }); + }); + + test("preserves stable alphabetical key order across metrics", async () => { + const resolution = resolveMetricConfig({ + sp: { + z_metric: { source: "demo.public.z_metric" }, + a_metric: { source: "demo.public.a_metric" }, + }, + }); + + const fetcher = async () => + mockDescribeResponse({ + columns: [{ name: "v", type: "DECIMAL", is_measure: true }], + }); + + const schemas = await syncMetrics(resolution, fetcher); + const bundle = buildMetricsMetadataBundle(schemas); + expect(Object.keys(bundle)).toEqual(["a_metric", "z_metric"]); + }); + + test("omits absent fields rather than emitting null/empty placeholders", async () => { + const resolution = resolveMetricConfig({ + sp: { revenue: { source: "demo.public.revenue" } }, + }); + + const fetcher = async () => + mockDescribeResponse({ + columns: [{ name: "arr", type: "DECIMAL", is_measure: true }], + }); + + const schemas = await syncMetrics(resolution, fetcher); + const bundle = buildMetricsMetadataBundle(schemas); + const arr = bundle.revenue.measures.arr; + expect(arr.type).toBe("DECIMAL"); + expect(arr.display_name).toBeUndefined(); + expect(arr.format).toBeUndefined(); + expect(arr.description).toBeUndefined(); + expect(arr.time_grain).toBeUndefined(); + }); + + test("only emits time_grain on time-typed dimensions, never on measures", async () => { + const resolution = resolveMetricConfig({ + sp: { revenue: { source: "demo.public.revenue" } }, + }); + + const fetcher = async () => + mockDescribeResponse({ + columns: [ + // Time-grain on a measure should not be picked up — measures never + // carry time_grain in the YAML 1.1 spec; defending here is belt- + // and-suspenders, in case DESCRIBE leaks a stray attribute. + { + name: "arr", + type: "DECIMAL", + is_measure: true, + time_grain: ["day"], + }, + { + name: "ts", + type: "TIMESTAMP", + is_measure: false, + time_grain: ["day", "month"], + }, + ], + }); + + const schemas = await syncMetrics(resolution, fetcher); + const bundle = buildMetricsMetadataBundle(schemas); + expect(bundle.revenue.measures.arr.time_grain).toBeUndefined(); + expect(bundle.revenue.dimensions.ts.time_grain).toEqual(["day", "month"]); + }); +}); + +// ── Phase 5: metadata JSON serialization ────────────────────────────────── +describe("generateMetricsMetadataJson — snapshot", () => { + test("serializes a representative metric view with display_name + format + time_grain", async () => { + const resolution = resolveMetricConfig({ + sp: { + revenue: { source: "appkit_demo.public.revenue_metrics" }, + }, + obo: { + customer_metrics: { + source: "appkit_demo.public.customer_metrics", + }, + }, + }); + + const fetcher = async (fqn: string) => + fqn.endsWith("revenue_metrics") + ? mockDescribeResponse({ + columns: [ + { + name: "arr", + type: "DECIMAL(38,2)", + is_measure: true, + display_name: "Annual Recurring Revenue", + format: "$#,##0.00", + comment: "ARR per quarter", + }, + { + name: "growth_rate", + type: "DOUBLE", + is_measure: true, + display_name: "Growth Rate", + format: "0.0%", + }, + { + name: "region", + type: "STRING", + is_measure: false, + display_name: "Region", + }, + { + name: "created_at", + type: "TIMESTAMP", + is_measure: false, + display_name: "Period", + time_grain: ["day", "week", "month", "quarter"], + }, + ], + }) + : mockDescribeResponse({ + columns: [ + { + name: "churn_rate", + type: "DOUBLE", + is_measure: true, + display_name: "Churn Rate", + format: "0.0%", + }, + { + name: "csm_email", + type: "STRING", + is_measure: false, + display_name: "CSM Email", + }, + ], + }); + + const schemas = await syncMetrics(resolution, fetcher); + const json = generateMetricsMetadataJson(schemas); + expect(json).toMatchSnapshot(); + + // Guard against snapshot blind-update: structural assertions on the parsed JSON. + const parsed = JSON.parse(json); + expect(Object.keys(parsed)).toEqual(["customer_metrics", "revenue"]); + expect(parsed.revenue.measures.arr.format).toBe("$#,##0.00"); + expect(parsed.revenue.measures.arr.display_name).toBe( + "Annual Recurring Revenue", + ); + // Time grains are sorted lexicographically by extractMetricColumns (Phase 2). + expect(parsed.revenue.dimensions.created_at.time_grain).toEqual([ + "day", + "month", + "quarter", + "week", + ]); + expect(parsed.customer_metrics.lane).toBe("obo"); + }); + + test("emits `{}` when no metrics are registered", () => { + expect(generateMetricsMetadataJson([])).toBe("{}\n"); + }); +}); + // ── Phase 2: syncMetrics propagates timeGrains end-to-end ──────────────── describe("syncMetrics — time-typed dimension propagation", () => { test("propagates the time_grain attribute onto the resulting MetricSchema", async () => { diff --git a/packages/appkit/src/type-generator/vite-plugin.ts b/packages/appkit/src/type-generator/vite-plugin.ts index 060896331..f9e75496e 100644 --- a/packages/appkit/src/type-generator/vite-plugin.ts +++ b/packages/appkit/src/type-generator/vite-plugin.ts @@ -5,6 +5,7 @@ import { createLogger } from "../logging/logger"; import { ANALYTICS_TYPES_FILE, generateFromEntryPoint, + METRIC_METADATA_FILE, METRIC_TYPES_FILE, TYPES_DIR, } from "./index"; @@ -19,6 +20,12 @@ interface AppKitTypesPluginOptions { outFile?: string; /** Path to the metric registry d.ts file (relative to client folder). */ metricOutFile?: string; + /** + * Path to the metric semantic-metadata JSON file (relative to client folder). + * Phase 5 build-time artifact — sibling of {@link metricOutFile}. Skipped + * automatically when `metric.json` is absent. + */ + metricMetadataOutFile?: string; /** Folders to watch for changes. */ watchFolders?: string[]; } @@ -32,6 +39,7 @@ interface AppKitTypesPluginOptions { export function appKitTypesPlugin(options?: AppKitTypesPluginOptions): Plugin { let outFile: string; let metricOutFile: string; + let metricMetadataOutFile: string; let watchFolders: string[]; async function generate() { @@ -49,6 +57,7 @@ export function appKitTypesPlugin(options?: AppKitTypesPluginOptions): Plugin { warehouseId, noCache: false, metricOutFile, + metricMetadataOutFile, }); } catch (error) { // throw in production to fail the build @@ -87,6 +96,11 @@ export function appKitTypesPlugin(options?: AppKitTypesPluginOptions): Plugin { projectRoot, options?.metricOutFile ?? `shared/${TYPES_DIR}/${METRIC_TYPES_FILE}`, ); + metricMetadataOutFile = path.resolve( + projectRoot, + options?.metricMetadataOutFile ?? + `shared/${TYPES_DIR}/${METRIC_METADATA_FILE}`, + ); watchFolders = options?.watchFolders ?? [ path.join(process.cwd(), "config", "queries"), ]; From 2d12c512652171d39c71c6c339114619d6bc8aea Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Thu, 30 Apr 2026 00:47:21 +0200 Subject: [PATCH 06/34] =?UTF-8?q?feat(shared):=20metric=20view=20source=20?= =?UTF-8?q?=E2=80=94=20Phase=206=20CLI=20metric=20sync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a top-level `metric` command to the AppKit CLI with a `sync` subcommand that exposes the same syncMetrics() core that the Vite plugin runs in dev mode. Useful for CI checks, non-Vite builds, manual refresh after pulling teammate's metric.json, and pre-commit hooks. Surface: npx appkit metric sync [--warehouse-id ] [--metric-json-path ] [--output-dir ] [--root-dir ] [--silent] [--json] Resolution order: CLI flags > env vars (DATABRICKS_*) > @clack/prompts. --silent / --json modes skip prompts and fail-fast on missing inputs. Validates metric.json against the JSON Schema (AJV) before invoking syncMetrics(); rejects malformed configs early without any network/SDK load. Wraps the Phase 1 fetcher to capture per-entry errors first-failure- wins, then classifies via heuristic message-substring match into 5 error modes: exit 0 success exit 1 missing-fqn (404 / "not found" / "does not exist") exit 2 warehouse-unreach (ECONNREFUSED / ETIMEDOUT) exit 3 malformed-config (JSON parse / schema validation / bare-string) exit 4 auth-failed (401 / 403 / token expired) exit 5 unknown (catch-all, original message preserved) Lazy-loads @databricks/appkit/type-generator (mirrors generate-types.ts) so the CLI compiles without appkit being built. MetricSyncDependencies test seam for unit tests. metric-source.schema.json copied into packages/shared/dist/schemas via tsdown so the validator finds it at runtime. Tests: 2075 total (+34 from Phase 5's 2041). 22 sync tests covering all five exit codes, stdout snapshots for success and multi-issue error, empty-metric.json no-op, flag-override paths. 12 validator tests covering 8 acceptance/rejection cases plus 4 error-formatter cases. Per prd/analytics-metric-view-source and tasks/.../Phase 6. xavier loop iteration 7. Co-authored-by: Isaac Signed-off-by: Atila Fassina --- .../shared/src/cli/commands/metric/index.ts | 23 + .../sync/__snapshots__/sync.test.ts.snap | 9 + .../validate-metric-source.test.ts.snap | 8 + .../src/cli/commands/metric/sync/sync.test.ts | 510 ++++++++++++++ .../src/cli/commands/metric/sync/sync.ts | 663 ++++++++++++++++++ .../sync/validate-metric-source.test.ts | 102 +++ .../metric/sync/validate-metric-source.ts | 151 ++++ .../src/cli/commands/type-generator.d.ts | 64 ++ packages/shared/src/cli/index.ts | 2 + packages/shared/tsdown.config.ts | 4 + 10 files changed, 1536 insertions(+) create mode 100644 packages/shared/src/cli/commands/metric/index.ts create mode 100644 packages/shared/src/cli/commands/metric/sync/__snapshots__/sync.test.ts.snap create mode 100644 packages/shared/src/cli/commands/metric/sync/__snapshots__/validate-metric-source.test.ts.snap create mode 100644 packages/shared/src/cli/commands/metric/sync/sync.test.ts create mode 100644 packages/shared/src/cli/commands/metric/sync/sync.ts create mode 100644 packages/shared/src/cli/commands/metric/sync/validate-metric-source.test.ts create mode 100644 packages/shared/src/cli/commands/metric/sync/validate-metric-source.ts diff --git a/packages/shared/src/cli/commands/metric/index.ts b/packages/shared/src/cli/commands/metric/index.ts new file mode 100644 index 000000000..ce57743d9 --- /dev/null +++ b/packages/shared/src/cli/commands/metric/index.ts @@ -0,0 +1,23 @@ +import { Command } from "commander"; +import { metricSyncCommand } from "./sync/sync"; + +/** + * Parent command for metric-view operations. + * + * Currently exposes a single subcommand (`sync`); future v1+ subcommands + * (`list`, `validate`, `describe`) plug in here so users have a single + * top-level surface for everything related to UC Metric Views. + * + * Sibling of `plugin`, `setup`, `generate-types`, `lint`, `docs`, `codemod`. + */ +export const metricCommand = new Command("metric") + .description("Metric-view management commands (UC Metric Views)") + .addCommand(metricSyncCommand) + .addHelpText( + "after", + ` +Examples: + $ appkit metric sync + $ appkit metric sync --warehouse-id 1234abcd5678efgh --metric-json-path config/queries/metric.json + $ appkit metric sync --silent`, + ); diff --git a/packages/shared/src/cli/commands/metric/sync/__snapshots__/sync.test.ts.snap b/packages/shared/src/cli/commands/metric/sync/__snapshots__/sync.test.ts.snap new file mode 100644 index 000000000..6eab08804 --- /dev/null +++ b/packages/shared/src/cli/commands/metric/sync/__snapshots__/sync.test.ts.snap @@ -0,0 +1,9 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`runMetricSync — success > produces a stable success-stdout snapshot 1`] = ` +"Syncing N metric(s) from config/queries/metric.json via warehouse stub-warehouse... +✓ Wrote shared/appkit-types/metric.d.ts +✓ Wrote shared/appkit-types/metrics.metadata.json" +`; + +exports[`runMetricSync — success > treats an empty metric.json as a no-op (no fetch call) 1`] = `"No metric entries found. Nothing to sync."`; diff --git a/packages/shared/src/cli/commands/metric/sync/__snapshots__/validate-metric-source.test.ts.snap b/packages/shared/src/cli/commands/metric/sync/__snapshots__/validate-metric-source.test.ts.snap new file mode 100644 index 000000000..3fefd9e39 --- /dev/null +++ b/packages/shared/src/cli/commands/metric/sync/__snapshots__/validate-metric-source.test.ts.snap @@ -0,0 +1,8 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`formatMetricSourceErrors > produces a stable error message for a multi-issue input 1`] = ` +" sp: does not match expected pattern (must match pattern "^[a-zA-Z_][a-zA-Z0-9_]*$") + sp: property name must be valid + sp.revenue: missing required property "source" + sp.1bad.source: does not match expected pattern (must match pattern "^[a-zA-Z0-9_][a-zA-Z0-9_-]*\\.[a-zA-Z0-9_][a-zA-Z0-9_-]*\\.[a-zA-Z0-9_][a-zA-Z0-9_-]*$")" +`; diff --git a/packages/shared/src/cli/commands/metric/sync/sync.test.ts b/packages/shared/src/cli/commands/metric/sync/sync.test.ts new file mode 100644 index 000000000..500ce42c5 --- /dev/null +++ b/packages/shared/src/cli/commands/metric/sync/sync.test.ts @@ -0,0 +1,510 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + classifyFetchError, + exitCodeFor, + type MetricSyncDependencies, + MetricSyncError, + runMetricSync, +} from "./sync"; + +// ── Helpers ──────────────────────────────────────────────────────────────── + +function makeTempDir(prefix: string): string { + return fs.mkdtempSync(path.join(os.tmpdir(), `${prefix}-`)); +} + +function cleanDir(dir: string): void { + try { + fs.rmSync(dir, { recursive: true, force: true }); + } catch { + // best-effort + } +} + +/** + * Build a fully-mocked {@link MetricSyncDependencies} that records what was + * called. Tests override individual fields (e.g. swap in a throwing fetcher + * factory to simulate auth-failed) without re-stating the rest. + */ +function makeDeps( + overrides: Partial = {}, +): MetricSyncDependencies { + return { + syncMetrics: vi.fn(async (resolution, fetcher) => { + // Default: call the fetcher once per entry and emit a stub schema. + const schemas: Array<{ + key: string; + source: string; + lane: "sp" | "obo"; + measures: never[]; + dimensions: never[]; + }> = []; + for (const entry of resolution.entries) { + try { + await fetcher(entry.source); + } catch { + // mirror the real syncMetrics behavior — it tolerates per-entry + // failures and emits empty schemas. The CLI's wrapped fetcher + // captures the first failure and re-throws after this returns. + } + schemas.push({ + key: entry.key, + source: entry.source, + lane: entry.lane, + measures: [], + dimensions: [], + }); + } + return schemas; + }), + resolveMetricConfig: vi.fn((config) => { + const cfg = config as { + sp?: Record; + obo?: Record; + }; + // Mirror the real `resolveMetricConfig`: sp first, then obo, each + // alphabetically sorted by key. This is the contract callers (and + // syncMetrics) rely on for deterministic ordering. + const entries: Array<{ + key: string; + source: string; + lane: "sp" | "obo"; + }> = []; + for (const lane of ["sp", "obo"] as const) { + const laneMap = cfg[lane] ?? {}; + for (const key of Object.keys(laneMap).sort()) { + entries.push({ key, source: laneMap[key].source, lane }); + } + } + return { entries }; + }), + createWorkspaceDescribeFetcher: vi.fn(() => async (_fqn: string) => ({ + ok: true, + })), + generateMetricTypeDeclarations: vi.fn(() => "// generated metric.d.ts\n"), + generateMetricsMetadataJson: vi.fn(() => "{}\n"), + metricTypesFile: "metric.d.ts", + metricMetadataFile: "metrics.metadata.json", + ...overrides, + }; +} + +/** + * Capture console writes through the IO seam so snapshots are deterministic. + */ +function captureIO() { + const log: string[] = []; + const error: string[] = []; + return { + log: (msg: string) => log.push(msg), + error: (msg: string) => error.push(msg), + output: () => log.join("\n"), + errors: () => error.join("\n"), + }; +} + +const VALID_METRIC_JSON = { + $schema: + "https://databricks.github.io/appkit/schemas/metric-source.schema.json", + sp: { + revenue: { source: "demo.public.revenue" }, + }, + obo: { + customer_metrics: { source: "demo.public.customer_metrics" }, + }, +}; + +// ── classifyFetchError ───────────────────────────────────────────────────── + +describe("classifyFetchError", () => { + it("classifies 401 unauthorized as auth-failed", () => { + const err = new Error("Request failed: 401 Unauthorized"); + const classified = classifyFetchError(err, "demo.public.x"); + expect(classified.code).toBe("auth-failed"); + expect(classified.message).toMatch(/Authentication failed/); + }); + + it("classifies 403 forbidden as auth-failed", () => { + const err = new Error("HTTP 403 forbidden"); + expect(classifyFetchError(err, "x.y.z").code).toBe("auth-failed"); + }); + + it("classifies token-expired as auth-failed", () => { + const err = new Error("OAuth token expired; please refresh"); + expect(classifyFetchError(err, "x.y.z").code).toBe("auth-failed"); + }); + + it("classifies 'not found' as missing-fqn", () => { + const err = new Error("TABLE_OR_VIEW_NOT_FOUND: relation x.y.z not found"); + const classified = classifyFetchError(err, "x.y.z"); + expect(classified.code).toBe("missing-fqn"); + expect(classified.message).toContain("'x.y.z'"); + }); + + it("classifies 'does not exist' as missing-fqn", () => { + const err = new Error("Table x.y.z does not exist"); + expect(classifyFetchError(err, "x.y.z").code).toBe("missing-fqn"); + }); + + it("classifies ECONNREFUSED as warehouse-unreach", () => { + const err = new Error("connect ECONNREFUSED 127.0.0.1:443"); + expect(classifyFetchError(err, "x.y.z").code).toBe("warehouse-unreach"); + }); + + it("classifies ETIMEDOUT as warehouse-unreach", () => { + const err = new Error("Request ETIMEDOUT after 30s"); + expect(classifyFetchError(err, "x.y.z").code).toBe("warehouse-unreach"); + }); + + it("classifies unknown errors as unknown", () => { + const err = new Error("Unexpected internal error"); + expect(classifyFetchError(err, "x.y.z").code).toBe("unknown"); + }); +}); + +// ── exitCodeFor ──────────────────────────────────────────────────────────── + +describe("exitCodeFor", () => { + it("maps each MetricSyncErrorCode to its canonical exit code", () => { + expect(exitCodeFor("missing-fqn")).toBe(1); + expect(exitCodeFor("warehouse-unreach")).toBe(2); + expect(exitCodeFor("malformed-config")).toBe(3); + expect(exitCodeFor("auth-failed")).toBe(4); + expect(exitCodeFor("unknown")).toBe(5); + }); +}); + +// ── runMetricSync — happy paths ──────────────────────────────────────────── + +describe("runMetricSync — success", () => { + let tmp: string; + + beforeEach(() => { + tmp = makeTempDir("metric-sync-success"); + fs.mkdirSync(path.join(tmp, "config", "queries"), { recursive: true }); + fs.writeFileSync( + path.join(tmp, "config", "queries", "metric.json"), + JSON.stringify(VALID_METRIC_JSON, null, 2), + ); + }); + + afterEach(() => cleanDir(tmp)); + + it("emits metric.d.ts and metrics.metadata.json on success", async () => { + const io = captureIO(); + const deps = makeDeps(); + const ctx = await runMetricSync( + { + warehouseId: "stub-warehouse", + rootDir: tmp, + silent: true, + }, + { ...io, deps, interactive: false }, + ); + + expect(ctx.warehouseId).toBe("stub-warehouse"); + expect(fs.existsSync(ctx.metricTypesPath)).toBe(true); + expect(fs.existsSync(ctx.metricMetadataPath)).toBe(true); + expect(deps.syncMetrics).toHaveBeenCalledTimes(1); + }); + + it("produces a stable success-stdout snapshot", async () => { + const io = captureIO(); + const deps = makeDeps(); + await runMetricSync( + { + warehouseId: "stub-warehouse", + rootDir: tmp, + }, + { ...io, deps, interactive: false }, + ); + + // Normalize the warehouse-relative path component for portability. + const snapshot = io + .output() + .replace(/Syncing \d+ metric\(s\) from /g, "Syncing N metric(s) from "); + expect(snapshot).toMatchSnapshot(); + }); + + it("treats an empty metric.json as a no-op (no fetch call)", async () => { + fs.writeFileSync( + path.join(tmp, "config", "queries", "metric.json"), + JSON.stringify({ sp: {}, obo: {} }, null, 2), + ); + + const io = captureIO(); + const deps = makeDeps(); + await runMetricSync( + { + warehouseId: "stub-warehouse", + rootDir: tmp, + }, + { ...io, deps, interactive: false }, + ); + + expect(deps.syncMetrics).not.toHaveBeenCalled(); + expect(io.output()).toMatchSnapshot(); + }); + + it("respects --metric-json-path and --output-dir overrides", async () => { + const altDir = path.join(tmp, "alt-config"); + fs.mkdirSync(altDir, { recursive: true }); + const altPath = path.join(altDir, "metrics.json"); + fs.writeFileSync(altPath, JSON.stringify(VALID_METRIC_JSON, null, 2)); + + const altOut = path.join(tmp, "build-out"); + + const io = captureIO(); + const deps = makeDeps(); + const ctx = await runMetricSync( + { + warehouseId: "stub-warehouse", + metricJsonPath: altPath, + outputDir: altOut, + rootDir: tmp, + silent: true, + }, + { ...io, deps, interactive: false }, + ); + + expect(ctx.metricJsonPath).toBe(altPath); + expect(ctx.outputDir).toBe(altOut); + expect(fs.existsSync(path.join(altOut, "metric.d.ts"))).toBe(true); + expect(fs.existsSync(path.join(altOut, "metrics.metadata.json"))).toBe( + true, + ); + }); +}); + +// ── runMetricSync — failure modes ────────────────────────────────────────── + +describe("runMetricSync — failure modes", () => { + let tmp: string; + + beforeEach(() => { + tmp = makeTempDir("metric-sync-failure"); + fs.mkdirSync(path.join(tmp, "config", "queries"), { recursive: true }); + fs.writeFileSync( + path.join(tmp, "config", "queries", "metric.json"), + JSON.stringify(VALID_METRIC_JSON, null, 2), + ); + }); + + afterEach(() => cleanDir(tmp)); + + it("rejects malformed JSON with malformed-config", async () => { + fs.writeFileSync( + path.join(tmp, "config", "queries", "metric.json"), + "{not valid json", + ); + + const io = captureIO(); + await expect( + runMetricSync( + { + warehouseId: "stub-warehouse", + rootDir: tmp, + silent: true, + }, + { ...io, deps: makeDeps(), interactive: false }, + ), + ).rejects.toMatchObject({ + code: "malformed-config", + }); + }); + + it("rejects missing metric.json with malformed-config", async () => { + fs.unlinkSync(path.join(tmp, "config", "queries", "metric.json")); + + const io = captureIO(); + await expect( + runMetricSync( + { + warehouseId: "stub-warehouse", + rootDir: tmp, + silent: true, + }, + { ...io, deps: makeDeps(), interactive: false }, + ), + ).rejects.toMatchObject({ + code: "malformed-config", + }); + }); + + it("rejects schema-invalid metric.json with malformed-config", async () => { + fs.writeFileSync( + path.join(tmp, "config", "queries", "metric.json"), + JSON.stringify({ sp: { "1bad-key": { source: "x" } } }), + ); + + const io = captureIO(); + let captured: MetricSyncError | null = null; + try { + await runMetricSync( + { + warehouseId: "stub-warehouse", + rootDir: tmp, + silent: true, + }, + { ...io, deps: makeDeps(), interactive: false }, + ); + } catch (err) { + captured = err as MetricSyncError; + } + expect(captured).toBeInstanceOf(MetricSyncError); + expect(captured?.code).toBe("malformed-config"); + // Stable summary line, message body varies by AJV version so we don't + // snapshot the full error. + expect(captured?.message).toContain("Invalid metric.json"); + }); + + it("rejects metric.json with bare-string source as malformed-config", async () => { + fs.writeFileSync( + path.join(tmp, "config", "queries", "metric.json"), + JSON.stringify({ sp: { revenue: "demo.public.revenue" } }), + ); + + const io = captureIO(); + await expect( + runMetricSync( + { + warehouseId: "stub-warehouse", + rootDir: tmp, + silent: true, + }, + { ...io, deps: makeDeps(), interactive: false }, + ), + ).rejects.toMatchObject({ + code: "malformed-config", + }); + }); + + it("surfaces missing-fqn when the fetcher rejects with 'not found'", async () => { + const deps = makeDeps({ + createWorkspaceDescribeFetcher: vi.fn(() => async (_fqn: string) => { + throw new Error("TABLE_OR_VIEW_NOT_FOUND: relation does not exist"); + }), + }); + + const io = captureIO(); + let captured: MetricSyncError | null = null; + try { + await runMetricSync( + { + warehouseId: "stub-warehouse", + rootDir: tmp, + silent: true, + }, + { ...io, deps, interactive: false }, + ); + } catch (err) { + captured = err as MetricSyncError; + } + expect(captured?.code).toBe("missing-fqn"); + expect(captured?.fqn).toBe("demo.public.revenue"); + expect(captured?.message).toContain("demo.public.revenue"); + }); + + it("surfaces warehouse-unreach with the warehouse ID embedded", async () => { + const deps = makeDeps({ + createWorkspaceDescribeFetcher: vi.fn(() => async (_fqn: string) => { + throw new Error( + "connect ECONNREFUSED 127.0.0.1:443 unreachable warehouse", + ); + }), + }); + + const io = captureIO(); + let captured: MetricSyncError | null = null; + try { + await runMetricSync( + { + warehouseId: "wh-12345", + rootDir: tmp, + silent: true, + }, + { ...io, deps, interactive: false }, + ); + } catch (err) { + captured = err as MetricSyncError; + } + expect(captured?.code).toBe("warehouse-unreach"); + expect(captured?.message).toContain("'wh-12345'"); + }); + + it("surfaces auth-failed when the fetcher rejects with 401", async () => { + const deps = makeDeps({ + createWorkspaceDescribeFetcher: vi.fn(() => async (_fqn: string) => { + throw new Error("401 Unauthorized — invalid OAuth token"); + }), + }); + + const io = captureIO(); + let captured: MetricSyncError | null = null; + try { + await runMetricSync( + { + warehouseId: "wh-12345", + rootDir: tmp, + silent: true, + }, + { ...io, deps, interactive: false }, + ); + } catch (err) { + captured = err as MetricSyncError; + } + expect(captured?.code).toBe("auth-failed"); + expect(captured?.message).toContain("Authentication failed"); + }); + + it("surfaces unknown for an unexpected error", async () => { + const deps = makeDeps({ + createWorkspaceDescribeFetcher: vi.fn(() => async (_fqn: string) => { + throw new Error("internal error: kernel panic"); + }), + }); + + const io = captureIO(); + let captured: MetricSyncError | null = null; + try { + await runMetricSync( + { + warehouseId: "wh-12345", + rootDir: tmp, + silent: true, + }, + { ...io, deps, interactive: false }, + ); + } catch (err) { + captured = err as MetricSyncError; + } + expect(captured?.code).toBe("unknown"); + }); + + it("rejects --silent with no warehouse ID resolved", async () => { + const previousEnv = process.env.DATABRICKS_WAREHOUSE_ID; + delete process.env.DATABRICKS_WAREHOUSE_ID; + + try { + const io = captureIO(); + await expect( + runMetricSync( + { + rootDir: tmp, + silent: true, + }, + { ...io, deps: makeDeps(), interactive: false }, + ), + ).rejects.toMatchObject({ + code: "warehouse-unreach", + }); + } finally { + if (previousEnv !== undefined) { + process.env.DATABRICKS_WAREHOUSE_ID = previousEnv; + } + } + }); +}); diff --git a/packages/shared/src/cli/commands/metric/sync/sync.ts b/packages/shared/src/cli/commands/metric/sync/sync.ts new file mode 100644 index 000000000..2b2cde7b3 --- /dev/null +++ b/packages/shared/src/cli/commands/metric/sync/sync.ts @@ -0,0 +1,663 @@ +import fs from "node:fs"; +import path from "node:path"; +import process from "node:process"; +import { cancel, intro, isCancel, outro, text } from "@clack/prompts"; +import { Command } from "commander"; +import { + formatMetricSourceErrors, + validateMetricSource, +} from "./validate-metric-source"; + +/** + * Recognizable error categories surfaced from `syncMetrics()` and the CLI's + * preflight checks. The taxonomy maps 1:1 onto exit codes so wrappers + * (CI scripts, pre-commit hooks) can branch on the failure mode. + * + * Mapping: + * missing-fqn → 1 "Metric view '' not found or not accessible" + * warehouse-unreach → 2 "Could not reach SQL warehouse ''" + * malformed-config → 3 "Invalid metric.json" + * auth-failed → 4 "Authentication failed" + * unknown → 5 catch-all + */ +export type MetricSyncErrorCode = + | "missing-fqn" + | "warehouse-unreach" + | "malformed-config" + | "auth-failed" + | "unknown"; + +const EXIT_CODE_BY_CATEGORY: Record = { + "missing-fqn": 1, + "warehouse-unreach": 2, + "malformed-config": 3, + "auth-failed": 4, + unknown: 5, +}; + +/** + * Typed error wrapper used by the CLI to bubble a recognizable failure mode + * (and its associated exit code) up from helper functions to the command's + * top-level catch. + */ +export class MetricSyncError extends Error { + readonly code: MetricSyncErrorCode; + readonly fqn?: string; + + constructor(code: MetricSyncErrorCode, message: string, fqn?: string) { + super(message); + this.name = "MetricSyncError"; + this.code = code; + if (fqn !== undefined) this.fqn = fqn; + } +} + +/** + * Classify an arbitrary error thrown by the DescribeFetcher (i.e. the + * Statement Execution API call inside `createWorkspaceDescribeFetcher`) into + * a recognizable {@link MetricSyncErrorCode}. + * + * The classification is intentionally string-shaped (no SDK type imports) + * because: + * - the CLI runs as a thin wrapper and we don't want to pull the Databricks + * SDK into the shared CLI package's hot path; + * - the error shapes are fluid across SDK releases — matching on substrings + * of the message gives us a stable contract even when the SDK shifts. + * + * The resulting categorization is conservative: when nothing matches we fall + * through to "unknown" so the catch-all exit code (5) carries the original + * message verbatim. Callers should always preserve the underlying message in + * stderr — the category is just a routing key. + */ +export function classifyFetchError(err: Error, fqn: string): MetricSyncError { + const msg = (err.message ?? "").toLowerCase(); + + // Auth failure signals — these come from the SDK's bearer/OAuth flows or + // from the workspace returning 401/403 directly. Match before the more + // generic "not found" / "unreachable" buckets so an auth-flavored 403 + // doesn't get bucketed as warehouse-unreach. + if ( + msg.includes("unauthorized") || + msg.includes("authentication") || + msg.includes("403") || + msg.includes("401") || + msg.includes("forbidden") || + msg.includes("invalid_grant") || + (msg.includes("token") && + (msg.includes("expired") || msg.includes("invalid"))) + ) { + return new MetricSyncError( + "auth-failed", + `Authentication failed: ${err.message}`, + fqn, + ); + } + + // Missing FQN signals — a SQL "table not found" / "doesn't exist" comes + // back as a FAILED statement, but if the SDK throws on a 404-style HTTP + // we catch it here. Match on the FQN word itself when present. + if ( + msg.includes("not found") || + msg.includes("does not exist") || + msg.includes("doesn't exist") || + msg.includes("no such table") + ) { + return new MetricSyncError( + "missing-fqn", + `Metric view '${fqn}' not found or not accessible: ${err.message}`, + fqn, + ); + } + + // Warehouse-reach signals — connection failures, host unreachable, timeouts. + // The warehouse ID itself isn't part of the message, so we can't echo it + // here; the caller appends it when constructing the final message. + if ( + msg.includes("econnrefused") || + msg.includes("etimedout") || + msg.includes("enotfound") || + msg.includes("network") || + msg.includes("unreachable") || + (msg.includes("warehouse") && msg.includes("not")) + ) { + return new MetricSyncError( + "warehouse-unreach", + `Could not reach SQL warehouse: ${err.message}`, + fqn, + ); + } + + return new MetricSyncError("unknown", err.message, fqn); +} + +/** + * Read and parse `metric.json` from a path. Throws a {@link MetricSyncError} + * with `malformed-config` on missing/parse errors so the top-level catch can + * route to the right exit code. + */ +function readMetricJson(metricJsonPath: string): unknown { + let raw: string; + try { + raw = fs.readFileSync(metricJsonPath, "utf-8"); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + throw new MetricSyncError( + "malformed-config", + `Could not read metric.json at ${metricJsonPath}: ${msg}`, + ); + } + + try { + return JSON.parse(raw); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + throw new MetricSyncError( + "malformed-config", + `Failed to parse metric.json at ${metricJsonPath}: ${msg}`, + ); + } +} + +/** + * Resolve the metric.json path. Honors --metric-json-path, otherwise looks at + * the conventional `/config/queries/metric.json` location. + */ +function resolveMetricJsonPath(rootDir: string, override?: string): string { + if (override) { + return path.isAbsolute(override) + ? override + : path.resolve(rootDir, override); + } + return path.resolve(rootDir, "config", "queries", "metric.json"); +} + +/** + * Resolve the output directory for the generated `.d.ts` and metadata bundle. + * Defaults to `/shared/appkit-types` to match the Vite-plugin output + * convention (`appKitTypesPlugin` writes to that location by default). + */ +function resolveOutputDir(rootDir: string, override?: string): string { + if (override) { + return path.isAbsolute(override) + ? override + : path.resolve(rootDir, override); + } + return path.resolve(rootDir, "shared", "appkit-types"); +} + +/** + * Inputs produced by `runMetricSync`'s preflight phase: everything the + * implementation needs after env vars / flags / prompts have been resolved. + * + * Exported (along with `runMetricSync`) for snapshot tests that want to + * inject deterministic seams. + */ +export interface MetricSyncContext { + warehouseId: string; + metricJsonPath: string; + outputDir: string; + metricTypesPath: string; + metricMetadataPath: string; +} + +/** + * Minimal column-metadata shape — mirrors `MetricColumnMetadata` from + * `@databricks/appkit/type-generator`. Kept here (rather than imported) so + * the shared CLI package compiles even when @databricks/appkit isn't built. + */ +export interface MetricSyncColumnMetadata { + name: string; + type: string; + isMeasure: boolean; + description?: string; + displayName?: string; + format?: string; + timeGrains?: string[]; +} + +/** + * Minimal MetricSchema shape — mirrors `MetricSchema` from the type-generator + * package. Tests construct stubs with empty `measures` / `dimensions` arrays, + * and the structural compatibility carries through to the real implementation. + */ +export interface MetricSyncSchema { + key: string; + source: string; + lane: "sp" | "obo"; + measures: MetricSyncColumnMetadata[]; + dimensions: MetricSyncColumnMetadata[]; +} + +/** + * The subset of `@databricks/appkit/type-generator` that the CLI consumes. + * Defined as a structural interface so tests can substitute a mock without + * loading the full ESM module graph (which would require `@databricks/appkit` + * to be built before tests run). + */ +export interface MetricSyncDependencies { + syncMetrics: ( + resolution: { + entries: Array<{ key: string; source: string; lane: "sp" | "obo" }>; + }, + fetcher: (fqn: string) => Promise, + ) => Promise; + resolveMetricConfig: (config: unknown) => { + entries: Array<{ key: string; source: string; lane: "sp" | "obo" }>; + }; + createWorkspaceDescribeFetcher: ( + warehouseId: string, + ) => (fqn: string) => Promise; + generateMetricTypeDeclarations: (schemas: MetricSyncSchema[]) => string; + generateMetricsMetadataJson: (schemas: MetricSyncSchema[]) => string; + metricTypesFile: string; + metricMetadataFile: string; +} + +/** + * Lazy-load `@databricks/appkit/type-generator`. Mirrors the dynamic-import + * pattern in `generate-types.ts` so the CLI does not hard-depend on the + * appkit package being installed (the published `appkit` CLI does, but the + * raw `shared` CLI package needs to compile cleanly without it). + */ +async function loadDefaultDependencies(): Promise { + try { + const mod = await import("@databricks/appkit/type-generator"); + return { + syncMetrics: mod.syncMetrics, + resolveMetricConfig: + mod.resolveMetricConfig as MetricSyncDependencies["resolveMetricConfig"], + createWorkspaceDescribeFetcher: mod.createWorkspaceDescribeFetcher, + generateMetricTypeDeclarations: mod.generateMetricTypeDeclarations, + generateMetricsMetadataJson: mod.generateMetricsMetadataJson, + metricTypesFile: mod.METRIC_TYPES_FILE, + metricMetadataFile: mod.METRIC_METADATA_FILE, + }; + } catch (err) { + if (err instanceof Error && err.message.includes("Cannot find module")) { + throw new MetricSyncError( + "unknown", + "The 'metric sync' command requires @databricks/appkit. Install it to use this command.", + ); + } + throw err; + } +} + +/** + * The fully-resolved set of flag/env/prompt inputs the command needs. + * + * `metricJsonPath` and `outputDir` are required (env-var-or-flag-or-prompt + * resolved); `warehouseId` is also required because `syncMetrics()` cannot + * issue the DESCRIBE without it. + */ +interface ResolvedInputs { + warehouseId: string; + metricJsonPath: string; + outputDir: string; + rootDir: string; +} + +/** + * Resolve inputs from the priority chain: explicit flags > env vars > interactive + * prompts. Matches the convention used by `plugin sync` and `plugin add-resource` + * — no prompt fires when the value is already known via flag or env. + * + * In `--silent` / `--json` modes we skip prompts entirely and surface a + * malformed-config error if a required field is unresolved (the wrapper script + * shouldn't see TTY prompts). + */ +async function resolveInputs( + options: MetricSyncFlags, + rootDir: string, + silent: boolean, +): Promise { + // Warehouse ID: --warehouse-id > DATABRICKS_WAREHOUSE_ID env var > prompt + let warehouseId = + options.warehouseId ?? process.env.DATABRICKS_WAREHOUSE_ID ?? ""; + + // metric.json path: --metric-json-path > /config/queries/metric.json + let metricJsonPath = resolveMetricJsonPath(rootDir, options.metricJsonPath); + + // Output dir: --output-dir > /shared/appkit-types + const outputDir = resolveOutputDir(rootDir, options.outputDir); + + if (silent) { + if (!warehouseId) { + throw new MetricSyncError( + "warehouse-unreach", + "No warehouse ID. Set DATABRICKS_WAREHOUSE_ID, pass --warehouse-id , or run interactively.", + ); + } + return { warehouseId, metricJsonPath, outputDir, rootDir }; + } + + // Interactive: only prompt for fields that weren't already resolved. + if (!warehouseId) { + const answer = await text({ + message: "Databricks SQL Warehouse ID?", + placeholder: "e.g. 1234abcd5678efgh", + validate(value) { + if (!value || value.trim().length === 0) { + return "Warehouse ID is required"; + } + return undefined; + }, + }); + if (isCancel(answer)) { + cancel("Cancelled."); + process.exit(0); + } + warehouseId = (answer as string).trim(); + } + + if (!options.metricJsonPath) { + // Only prompt if the conventional location does not exist; otherwise we + // assume the user meant the default and proceed without nagging. + if (!fs.existsSync(metricJsonPath)) { + const answer = await text({ + message: "Path to metric.json?", + placeholder: "config/queries/metric.json", + initialValue: path.relative(rootDir, metricJsonPath), + validate(value) { + if (!value || value.trim().length === 0) { + return "metric.json path is required"; + } + return undefined; + }, + }); + if (isCancel(answer)) { + cancel("Cancelled."); + process.exit(0); + } + const resolved = (answer as string).trim(); + metricJsonPath = path.isAbsolute(resolved) + ? resolved + : path.resolve(rootDir, resolved); + } + } + + if (!options.outputDir) { + // Use the default — no prompt unless the user explicitly opts in via flag. + // Mirroring `generate-types.ts`'s convention. + } + + return { warehouseId, metricJsonPath, outputDir, rootDir }; +} + +/** + * CLI flags accepted by `appkit metric sync`. Exposed for test wiring. + */ +export interface MetricSyncFlags { + warehouseId?: string; + metricJsonPath?: string; + outputDir?: string; + rootDir?: string; + silent?: boolean; + json?: boolean; +} + +/** + * The full implementation of `appkit metric sync`. Pure-ish: takes a `deps` + * seam so tests can inject a mock {@link MetricSyncDependencies} and a mock + * console writer. Production wires {@link loadDefaultDependencies} and + * `console.log` / `console.error`. + * + * Design notes: + * - We deliberately do **not** start the dependency load until after the + * metric.json path / schema validation step. This keeps the CLI usable + * for "did I write a valid metric.json?" checks even in environments + * where `@databricks/appkit` is missing. + * - `syncMetrics()` is tolerant by design (it returns empty schemas on a + * per-entry fetch error). To surface those errors at the CLI seam, we + * wrap the fetcher to capture the first failure and re-throw a typed + * {@link MetricSyncError}; subsequent entries are skipped. + */ +export async function runMetricSync( + options: MetricSyncFlags, + io: { + log: (msg: string) => void; + error: (msg: string) => void; + deps?: MetricSyncDependencies; + interactive?: boolean; + }, +): Promise { + const rootDir = options.rootDir + ? path.resolve(options.rootDir) + : process.cwd(); + const silent = Boolean(options.silent || options.json); + const interactive = io.interactive ?? !silent; + + if (interactive) { + intro("Sync metric-view types"); + } + + const inputs = await resolveInputs(options, rootDir, !interactive); + + // Step 1: Read + validate metric.json against the JSON Schema. + const parsed = readMetricJson(inputs.metricJsonPath); + const schemaResult = validateMetricSource(parsed); + if (!schemaResult.valid || !schemaResult.config) { + const details = schemaResult.errors?.length + ? formatMetricSourceErrors(schemaResult.errors) + : "(no validator output)"; + throw new MetricSyncError( + "malformed-config", + `Invalid metric.json at ${inputs.metricJsonPath}:\n${details}`, + ); + } + + // Step 2: Load deps (or use the injected seam) and resolve the config into + // a MetricConfigResolution. `resolveMetricConfig` performs the additional + // structural checks the JSON Schema can't express (duplicate keys across + // lanes, unknown fields). It throws plain Error; we re-shape into + // malformed-config so the CLI surfaces the right exit code. + const deps = io.deps ?? (await loadDefaultDependencies()); + + let resolution: ReturnType; + try { + resolution = deps.resolveMetricConfig(schemaResult.config); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + throw new MetricSyncError( + "malformed-config", + `Invalid metric.json at ${inputs.metricJsonPath}: ${msg}`, + ); + } + + if (resolution.entries.length === 0) { + if (!silent) { + io.log("No metric entries found. Nothing to sync."); + } + if (interactive) { + outro("Done."); + } + return { + warehouseId: inputs.warehouseId, + metricJsonPath: inputs.metricJsonPath, + outputDir: inputs.outputDir, + metricTypesPath: path.join(inputs.outputDir, deps.metricTypesFile), + metricMetadataPath: path.join(inputs.outputDir, deps.metricMetadataFile), + }; + } + + // Step 3: Build a fetcher that classifies the first failure into a typed + // MetricSyncError. We can't rely on `syncMetrics()` to throw — it captures + // and continues — so we wrap before passing it in. Only the *first* failure + // wins so the surfaced exit code reflects the earliest problem the user + // hit (subsequent entries are best-effort and may show different symptoms). + const baseFetcher = deps.createWorkspaceDescribeFetcher(inputs.warehouseId); + let firstFailure: MetricSyncError | null = null; + const wrappedFetcher: (fqn: string) => Promise = async (fqn) => { + try { + return await baseFetcher(fqn); + } catch (err) { + const e = err instanceof Error ? err : new Error(String(err)); + if (firstFailure === null) { + const classified = classifyFetchError(e, fqn); + // Refine warehouse-unreach to include the warehouse ID in the message + // (the SDK's error doesn't carry it). + firstFailure = + classified.code === "warehouse-unreach" + ? new MetricSyncError( + "warehouse-unreach", + `Could not reach SQL warehouse '${inputs.warehouseId}': ${e.message}`, + fqn, + ) + : classified; + } + throw e; + } + }; + + if (!silent) { + io.log( + `Syncing ${resolution.entries.length} metric(s) from ${path.relative(rootDir, inputs.metricJsonPath)} via warehouse ${inputs.warehouseId}...`, + ); + } + + const schemas = await deps.syncMetrics(resolution, wrappedFetcher); + + // If any entry's fetch failed, surface the first failure as a typed error. + // We deliberately defer this until after `syncMetrics()` returns so the + // emitted artifact (if we choose to emit it) reflects what we know. + if (firstFailure) { + throw firstFailure; + } + + // Step 4: Emit artifacts. `outputDir` is created (recursively) on first use. + fs.mkdirSync(inputs.outputDir, { recursive: true }); + + const metricTypesPath = path.join(inputs.outputDir, deps.metricTypesFile); + const metricMetadataPath = path.join( + inputs.outputDir, + deps.metricMetadataFile, + ); + + fs.writeFileSync( + metricTypesPath, + deps.generateMetricTypeDeclarations(schemas), + "utf-8", + ); + fs.writeFileSync( + metricMetadataPath, + deps.generateMetricsMetadataJson(schemas), + "utf-8", + ); + + if (!silent) { + io.log(`✓ Wrote ${path.relative(rootDir, metricTypesPath)}`); + io.log(`✓ Wrote ${path.relative(rootDir, metricMetadataPath)}`); + } + + if (interactive) { + outro(`Synced ${schemas.length} metric(s).`); + } + + return { + warehouseId: inputs.warehouseId, + metricJsonPath: inputs.metricJsonPath, + outputDir: inputs.outputDir, + metricTypesPath, + metricMetadataPath, + }; +} + +/** + * Map a {@link MetricSyncErrorCode} to the canonical exit code. Test consumers + * import this directly to assert exit-code expectations without spawning a + * subprocess. + */ +export function exitCodeFor(code: MetricSyncErrorCode): number { + return EXIT_CODE_BY_CATEGORY[code]; +} + +export const metricSyncCommand = new Command("sync") + .description( + "Sync metric-view schemas from Databricks: fetch DESCRIBE TABLE EXTENDED for every entry in metric.json, then emit metric.d.ts + metrics.metadata.json.", + ) + .option( + "--warehouse-id ", + "Databricks SQL Warehouse ID (overrides DATABRICKS_WAREHOUSE_ID env var)", + ) + .option( + "--metric-json-path ", + "Path to metric.json (default: config/queries/metric.json)", + ) + .option( + "--output-dir ", + "Output directory for metric.d.ts and metrics.metadata.json (default: shared/appkit-types)", + ) + .option( + "--root-dir ", + "Project root used to resolve relative defaults (default: cwd)", + ) + .option( + "-s, --silent", + "Suppress non-error output and never enter interactive mode", + ) + .option("--json", "Emit a single-line JSON summary on success") + .addHelpText( + "after", + ` +Examples: + $ appkit metric sync + $ appkit metric sync --warehouse-id 1234abcd5678efgh + $ appkit metric sync --metric-json-path config/queries/metric.json + $ appkit metric sync --output-dir shared/appkit-types --silent + +Environment variables: + DATABRICKS_WAREHOUSE_ID SQL warehouse ID (used when --warehouse-id is omitted) + DATABRICKS_HOST Databricks workspace URL (consumed by the SDK)`, + ) + .action((opts: MetricSyncFlags) => { + runMetricSync(opts, { + log: (msg) => { + if (opts.json) return; + console.log(msg); + }, + error: (msg) => console.error(msg), + }) + .then((ctx) => { + if (opts.json) { + console.log( + JSON.stringify({ + ok: true, + warehouseId: ctx.warehouseId, + metricJsonPath: ctx.metricJsonPath, + outputDir: ctx.outputDir, + metricTypesPath: ctx.metricTypesPath, + metricMetadataPath: ctx.metricMetadataPath, + }), + ); + } + process.exit(0); + }) + .catch((err: unknown) => { + if (err instanceof MetricSyncError) { + if (opts.json) { + console.log( + JSON.stringify({ + ok: false, + code: err.code, + message: err.message, + ...(err.fqn ? { fqn: err.fqn } : {}), + }), + ); + } else { + console.error(`Error: ${err.message}`); + } + process.exit(exitCodeFor(err.code)); + } + + // Unexpected — preserve the raw error and exit 5. + const msg = err instanceof Error ? err.message : String(err); + if (opts.json) { + console.log( + JSON.stringify({ ok: false, code: "unknown", message: msg }), + ); + } else { + console.error(`Error: ${msg}`); + } + process.exit(exitCodeFor("unknown")); + }); + }); diff --git a/packages/shared/src/cli/commands/metric/sync/validate-metric-source.test.ts b/packages/shared/src/cli/commands/metric/sync/validate-metric-source.test.ts new file mode 100644 index 000000000..0986b192c --- /dev/null +++ b/packages/shared/src/cli/commands/metric/sync/validate-metric-source.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, it } from "vitest"; +import { + formatMetricSourceErrors, + validateMetricSource, +} from "./validate-metric-source"; + +describe("validateMetricSource", () => { + it("accepts a valid SP-only configuration", () => { + const result = validateMetricSource({ + sp: { revenue: { source: "demo.public.revenue" } }, + }); + expect(result.valid).toBe(true); + expect(result.config).toBeDefined(); + }); + + it("accepts an empty configuration", () => { + expect(validateMetricSource({}).valid).toBe(true); + expect(validateMetricSource({ sp: {}, obo: {} }).valid).toBe(true); + }); + + it("rejects null/non-object inputs", () => { + expect(validateMetricSource(null).valid).toBe(false); + expect(validateMetricSource(undefined).valid).toBe(false); + expect(validateMetricSource("not an object").valid).toBe(false); + expect(validateMetricSource([1, 2, 3]).valid).toBe(false); + }); + + it("rejects bare-string source entries (must be {source})", () => { + const result = validateMetricSource({ + sp: { revenue: "demo.public.revenue" }, + }); + expect(result.valid).toBe(false); + expect(result.errors?.length).toBeGreaterThan(0); + }); + + it("rejects entries with unknown fields (closed v1 contract)", () => { + const result = validateMetricSource({ + sp: { revenue: { source: "demo.public.revenue", cacheTtl: 60 } }, + }); + expect(result.valid).toBe(false); + }); + + it("rejects metric keys starting with a digit", () => { + const result = validateMetricSource({ + sp: { "1bad-key": { source: "demo.public.revenue" } }, + }); + expect(result.valid).toBe(false); + }); + + it("rejects non-three-part FQNs", () => { + const result = validateMetricSource({ + sp: { revenue: { source: "two.parts" } }, + }); + expect(result.valid).toBe(false); + }); + + it("rejects unknown top-level keys", () => { + const result = validateMetricSource({ + sp: {}, + obo: {}, + extra: {}, + }); + expect(result.valid).toBe(false); + }); +}); + +describe("formatMetricSourceErrors", () => { + it("formats a 'required' error with property name", () => { + const result = validateMetricSource({ + sp: { revenue: {} }, + }); + expect(result.valid).toBe(false); + const formatted = formatMetricSourceErrors(result.errors ?? []); + expect(formatted).toContain('missing required property "source"'); + }); + + it("formats an 'additionalProperties' error with property name", () => { + const result = validateMetricSource({ + sp: { revenue: { source: "demo.public.revenue", cacheTtl: 60 } }, + }); + expect(result.valid).toBe(false); + const formatted = formatMetricSourceErrors(result.errors ?? []); + expect(formatted).toContain('unknown property "cacheTtl"'); + }); + + it("formats a 'pattern' error", () => { + const result = validateMetricSource({ + sp: { revenue: { source: "two.parts" } }, + }); + expect(result.valid).toBe(false); + const formatted = formatMetricSourceErrors(result.errors ?? []); + expect(formatted).toContain("does not match expected pattern"); + }); + + it("produces a stable error message for a multi-issue input", () => { + const result = validateMetricSource({ + sp: { revenue: {}, "1bad": { source: "two.parts" } }, + }); + expect(result.valid).toBe(false); + expect(formatMetricSourceErrors(result.errors ?? [])).toMatchSnapshot(); + }); +}); diff --git a/packages/shared/src/cli/commands/metric/sync/validate-metric-source.ts b/packages/shared/src/cli/commands/metric/sync/validate-metric-source.ts new file mode 100644 index 000000000..d87b145f0 --- /dev/null +++ b/packages/shared/src/cli/commands/metric/sync/validate-metric-source.ts @@ -0,0 +1,151 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import Ajv, { type ErrorObject } from "ajv"; +import addFormats from "ajv-formats"; +import type { MetricSourceConfiguration } from "../../../../schemas/metric-source.generated"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +/** + * Resolve the metric-source schema path. The schema is copied to `dist/schemas` + * by tsdown at build time so the published CLI can locate it; in dev (running + * from `src/`) we walk back to the `src/schemas` checkout. + */ +const SCHEMAS_DIR = path.join(__dirname, "..", "..", "..", "..", "schemas"); +const METRIC_SOURCE_SCHEMA_PATH = path.join( + SCHEMAS_DIR, + "metric-source.schema.json", +); + +export interface ValidateMetricSourceResult { + valid: boolean; + config?: MetricSourceConfiguration; + errors?: ErrorObject[]; +} + +let compiledValidator: ReturnType | null = null; +let schemaLoadWarned = false; + +function loadSchema(schemaPath: string): object | null { + try { + return JSON.parse(fs.readFileSync(schemaPath, "utf-8")) as object; + } catch (err) { + if (!schemaLoadWarned) { + schemaLoadWarned = true; + const msg = err instanceof Error ? err.message : String(err); + console.warn( + `Warning: Could not load metric-source schema at ${schemaPath}: ${msg}. Falling back to basic validation.`, + ); + } + return null; + } +} + +function getValidator(): ReturnType | null { + if (compiledValidator) return compiledValidator; + const schema = loadSchema(METRIC_SOURCE_SCHEMA_PATH); + if (!schema) return null; + try { + const ajv = new Ajv({ allErrors: true, strict: false }); + addFormats(ajv); + compiledValidator = ajv.compile(schema); + return compiledValidator; + } catch { + return null; + } +} + +/** + * Validate a parsed metric.json object against the metric-source JSON Schema. + * + * The schema is the canonical contract — any malformed input is rejected at the + * CLI seam before we hand off to `syncMetrics()`. This mirrors the plugin + * `validate-manifest` pattern: when the schema cannot be loaded (e.g. dist not + * built yet) we fall back to a structural check so the CLI is still usable in + * mid-build situations, but the full schema is the source of truth. + */ +export function validateMetricSource(obj: unknown): ValidateMetricSourceResult { + if (!obj || typeof obj !== "object" || Array.isArray(obj)) { + return { + valid: false, + errors: [ + { + instancePath: "", + message: "Metric source is not a valid object", + } as ErrorObject, + ], + }; + } + + const validate = getValidator(); + if (!validate) { + // Defensive fallback when the schema can't be loaded — accept any object + // that has the rough sp/obo shape so the CLI does not hard-fail in a + // partially-built tree. The dedicated schema test exercises the strict + // path; this branch only fires in development edge cases. + const m = obj as Record; + const spOk = + m.sp == null || (typeof m.sp === "object" && !Array.isArray(m.sp)); + const oboOk = + m.obo == null || (typeof m.obo === "object" && !Array.isArray(m.obo)); + if (spOk && oboOk) { + return { valid: true, config: obj as MetricSourceConfiguration }; + } + return { + valid: false, + errors: [ + { + instancePath: "", + message: "Invalid metric.json structure", + } as ErrorObject, + ], + }; + } + + const valid = validate(obj); + if (valid) return { valid: true, config: obj as MetricSourceConfiguration }; + return { valid: false, errors: validate.errors ?? [] }; +} + +/** + * Convert a JSON pointer like /sp/revenue/source to a readable path + * like sp.revenue.source for CLI output. + */ +function humanizePath(instancePath: string): string { + if (!instancePath) return "(root)"; + return instancePath.replace(/^\//, "").replace(/\//g, "."); +} + +/** + * Format AJV errors for CLI output. + * + * The output is a multi-line block (one issue per line, two-space indent) so + * it can be embedded directly under a "Invalid metric.json:" header by the + * caller. Mirrors the plugin manifest validator's formatter shape so the CLI + * UX stays consistent across `plugin validate` and `metric sync`. + */ +export function formatMetricSourceErrors(errors: ErrorObject[]): string { + const lines: string[] = []; + for (const err of errors) { + const readable = humanizePath(err.instancePath); + if (err.keyword === "required") { + lines.push( + ` ${readable}: missing required property "${err.params?.missingProperty}"`, + ); + } else if (err.keyword === "additionalProperties") { + lines.push( + ` ${readable}: unknown property "${err.params?.additionalProperty}"`, + ); + } else if (err.keyword === "pattern") { + lines.push( + ` ${readable}: does not match expected pattern${err.message ? ` (${err.message})` : ""}`, + ); + } else if (err.keyword === "type") { + lines.push(` ${readable}: expected type "${err.params?.type}"`); + } else { + lines.push(` ${readable}: ${err.message ?? "validation error"}`); + } + } + return lines.join("\n"); +} diff --git a/packages/shared/src/cli/commands/type-generator.d.ts b/packages/shared/src/cli/commands/type-generator.d.ts index ce69781fa..3608b970f 100644 --- a/packages/shared/src/cli/commands/type-generator.d.ts +++ b/packages/shared/src/cli/commands/type-generator.d.ts @@ -11,4 +11,68 @@ declare module "@databricks/appkit/type-generator" { outFile: string; noCache?: boolean; }): Promise; + + // ── Metric-view sync seam (consumed by the `metric sync` CLI subcommand) ── + /** + * Single column emitted into the build-time metric registry / metadata bundle. + * Mirrors `MetricColumnMetadata` in the type-generator package. + */ + export interface MetricColumnMetadata { + name: string; + type: string; + isMeasure: boolean; + description?: string; + displayName?: string; + format?: string; + timeGrains?: string[]; + } + + /** Per-metric schema captured at type-generation time. */ + export interface MetricSchema { + key: string; + source: string; + lane: "sp" | "obo"; + measures: MetricColumnMetadata[]; + dimensions: MetricColumnMetadata[]; + } + + /** Resolved entry consumed by the metric-view pipeline. */ + export interface MetricConfigResolution { + entries: Array<{ + key: string; + source: string; + lane: "sp" | "obo"; + }>; + } + + /** Shape of metric.json. */ + export interface MetricSourceConfig { + $schema?: string; + sp?: Record; + obo?: Record; + } + + export type DescribeFetcher = (fqn: string) => Promise; + + export function readMetricConfig( + queryFolder: string, + ): Promise; + export function resolveMetricConfig( + config: MetricSourceConfig, + ): MetricConfigResolution; + export function syncMetrics( + resolution: MetricConfigResolution, + fetcher: DescribeFetcher, + ): Promise; + export function createWorkspaceDescribeFetcher( + warehouseId: string, + ): DescribeFetcher; + export function generateMetricTypeDeclarations( + schemas: MetricSchema[], + ): string; + export function generateMetricsMetadataJson(schemas: MetricSchema[]): string; + + export const METRIC_TYPES_FILE: string; + export const METRIC_METADATA_FILE: string; + export const TYPES_DIR: string; } diff --git a/packages/shared/src/cli/index.ts b/packages/shared/src/cli/index.ts index 4d0ed65b7..64cbadd48 100644 --- a/packages/shared/src/cli/index.ts +++ b/packages/shared/src/cli/index.ts @@ -8,6 +8,7 @@ import { codemodCommand } from "./commands/codemod/index.js"; import { docsCommand } from "./commands/docs.js"; import { generateTypesCommand } from "./commands/generate-types.js"; import { lintCommand } from "./commands/lint.js"; +import { metricCommand } from "./commands/metric/index.js"; import { pluginCommand } from "./commands/plugin/index.js"; import { setupCommand } from "./commands/setup.js"; @@ -27,6 +28,7 @@ cmd.addCommand(generateTypesCommand); cmd.addCommand(lintCommand); cmd.addCommand(docsCommand); cmd.addCommand(pluginCommand); +cmd.addCommand(metricCommand); cmd.addCommand(codemodCommand); cmd.parse(); diff --git a/packages/shared/tsdown.config.ts b/packages/shared/tsdown.config.ts index d118f7ab9..28e3cd604 100644 --- a/packages/shared/tsdown.config.ts +++ b/packages/shared/tsdown.config.ts @@ -30,5 +30,9 @@ export default defineConfig({ from: "src/schemas/template-plugins.schema.json", to: "dist/schemas", }, + { + from: "src/schemas/metric-source.schema.json", + to: "dist/schemas", + }, ], }); From 79eeebf6910a32f3e62cf1956354ba7f85244ac2 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Thu, 30 Apr 2026 01:07:08 +0200 Subject: [PATCH 07/34] =?UTF-8?q?feat(playground):=20metric=20view=20sourc?= =?UTF-8?q?e=20=E2=80=94=20Phase=207=20docs=20+=20dev-playground=20integra?= =?UTF-8?q?tion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wrap the framework feature with documentation and a working reference app demo. With this commit, the Metric View Source feature is shippable as v1 end to end. docs/docs/plugins/analytics-metric-views.md covers all 8 sections per the PRD: metric.json shape, route contract, useMetricView hook reference, filter spec (12 ops + AND/OR examples), time-grain semantics, metadata flow + format utilities (Plotly + ECharts examples), CLI command reference (npx appkit metric sync), and security model. apps/dev-playground/client/src/routes/metrics.route.tsx demonstrates the full stack: - SP-lane revenue Plotly line chart with toD3Format / formatLabel helpers wiring metadata into Plotly's tickformat and trace name - OBO-lane customer metrics with current-user header - Hardcoded structured filter (region in EMEA/APAC/AMER) showing the spec - Graceful "could not load" fallback when the underlying UC metric view doesn't exist live in the dev workspace (the demo route still compiles and renders against the typed surfaces — the metric view existing in UC is decoupled from the feature working in code) apps/dev-playground/shared/appkit-types/{metrics.d.ts, metrics.metadata.json} are hand-authored equivalents of what `npx appkit metric sync` would emit if the demo metric views existed in the dev workspace. registerMetricsMetadata called once at startup in main.tsx; format utilities resolve correctly. Plotly (react-plotly.js + plotly.js + types) added to dev-playground/client only — not to @databricks/appkit-ui core. BI customers wire their own chart library; the framework stays library-agnostic. 5 Playwright tests in apps/dev-playground/tests/metrics.spec.ts cover the happy path (page load, chart render with formatted axis, OBO header) and the error path (graceful fallback). Both /api/analytics/metric/{revenue, customer_metrics} routes are exercised via mocked SSE responses. Backpressure: 2075/2075 tests, all 6 commands green. No regression in existing dev-playground routes. Per prd/analytics-metric-view-source and tasks/.../Phase 7. xavier loop iteration 8 — final. Co-authored-by: Isaac Signed-off-by: Atila Fassina --- apps/dev-playground/client/package.json | 5 +- apps/dev-playground/client/src/main.tsx | 17 + .../client/src/routeTree.gen.ts | 21 + .../client/src/routes/__root.tsx | 8 + .../client/src/routes/index.tsx | 18 + .../client/src/routes/metrics.route.tsx | 364 ++ apps/dev-playground/client/tsconfig.app.json | 1 + .../shared/appkit-types/metrics.d.ts | 135 + .../shared/appkit-types/metrics.metadata.json | 79 + apps/dev-playground/tests/metrics.spec.ts | 159 + docs/docs/plugins/analytics-metric-views.md | 585 ++++ docs/static/appkit-ui/styles.gen.css | 3 + pnpm-lock.yaml | 3067 ++++++++++++++++- 13 files changed, 4344 insertions(+), 118 deletions(-) create mode 100644 apps/dev-playground/client/src/routes/metrics.route.tsx create mode 100644 apps/dev-playground/shared/appkit-types/metrics.d.ts create mode 100644 apps/dev-playground/shared/appkit-types/metrics.metadata.json create mode 100644 apps/dev-playground/tests/metrics.spec.ts create mode 100644 docs/docs/plugins/analytics-metric-views.md diff --git a/apps/dev-playground/client/package.json b/apps/dev-playground/client/package.json index 9bf90c3fd..bf0470a20 100644 --- a/apps/dev-playground/client/package.json +++ b/apps/dev-playground/client/package.json @@ -21,8 +21,10 @@ "class-variance-authority": "0.7.1", "clsx": "2.1.1", "lucide-react": "0.546.0", + "plotly.js": "^3.5.0", "react": "19.2.0", "react-dom": "19.2.0", + "react-plotly.js": "^2.6.0", "recharts": "3.4.1", "tailwind-merge": "3.3.1", "tailwindcss-animate": "1.0.7", @@ -30,10 +32,12 @@ }, "devDependencies": { "@eslint/js": "9.36.0", + "@tailwindcss/postcss": "4.1.17", "@tanstack/router-cli": "1.133.20", "@types/node": "24.6.0", "@types/react": "19.2.2", "@types/react-dom": "19.2.2", + "@types/react-plotly.js": "^2.6.4", "@vitejs/plugin-react": "5.0.4", "autoprefixer": "10.4.21", "eslint": "9.36.0", @@ -43,7 +47,6 @@ "postcss": "8.5.6", "shiki": "3.15.0", "tailwindcss": "4.1.17", - "@tailwindcss/postcss": "4.1.17", "typescript": "5.9.3", "typescript-eslint": "8.45.0", "vite": "npm:rolldown-vite@7.1.14" diff --git a/apps/dev-playground/client/src/main.tsx b/apps/dev-playground/client/src/main.tsx index 5297b637a..30d056abc 100644 --- a/apps/dev-playground/client/src/main.tsx +++ b/apps/dev-playground/client/src/main.tsx @@ -1,9 +1,26 @@ +import { + type MetricsMetadataBundle, + registerMetricsMetadata, +} from "@databricks/appkit-ui/format"; import { createRouter, RouterProvider } from "@tanstack/react-router"; import React from "react"; import ReactDOM from "react-dom/client"; +// Build-time-emitted metadata bundle. In a production app this file is +// regenerated by `npx @databricks/appkit metric sync` (or the Vite plugin) +// from `config/queries/metric.json`. Phase 7 ships a hand-authored copy so +// the demo route at `/metrics` can wire format specs into Plotly even when +// the dev workspace does not host the underlying UC metric views. +import metricsMetadata from "../../shared/appkit-types/metrics.metadata.json"; import { routeTree } from "./routeTree.gen"; import "./index.css"; +// Register the metric metadata bundle once at startup so `useMetricView()`'s +// `metadata` field is populated for every consumer in the app. The cast is +// load-bearing: TypeScript widens JSON-imported string literals (`"sp"`) to +// `string`, which the bundle's `lane: "sp" | "obo"` discriminant rejects. +// The actual JSON values are hand-checked against the schema. +registerMetricsMetadata(metricsMetadata as unknown as MetricsMetadataBundle); + const router = createRouter({ routeTree, defaultPreload: "intent", diff --git a/apps/dev-playground/client/src/routeTree.gen.ts b/apps/dev-playground/client/src/routeTree.gen.ts index 45e280700..0ccf094cc 100644 --- a/apps/dev-playground/client/src/routeTree.gen.ts +++ b/apps/dev-playground/client/src/routeTree.gen.ts @@ -16,6 +16,7 @@ import { Route as SqlHelpersRouteRouteImport } from './routes/sql-helpers.route' import { Route as ServingRouteRouteImport } from './routes/serving.route' import { Route as ReconnectRouteRouteImport } from './routes/reconnect.route' import { Route as PolicyMatrixRouteRouteImport } from './routes/policy-matrix.route' +import { Route as MetricsRouteRouteImport } from './routes/metrics.route' import { Route as LakebaseRouteRouteImport } from './routes/lakebase.route' import { Route as JobsRouteRouteImport } from './routes/jobs.route' import { Route as GenieRouteRouteImport } from './routes/genie.route' @@ -61,6 +62,11 @@ const PolicyMatrixRouteRoute = PolicyMatrixRouteRouteImport.update({ path: '/policy-matrix', getParentRoute: () => rootRouteImport, } as any) +const MetricsRouteRoute = MetricsRouteRouteImport.update({ + id: '/metrics', + path: '/metrics', + getParentRoute: () => rootRouteImport, +} as any) const LakebaseRouteRoute = LakebaseRouteRouteImport.update({ id: '/lakebase', path: '/lakebase', @@ -117,6 +123,7 @@ export interface FileRoutesByFullPath { '/genie': typeof GenieRouteRoute '/jobs': typeof JobsRouteRoute '/lakebase': typeof LakebaseRouteRoute + '/metrics': typeof MetricsRouteRoute '/policy-matrix': typeof PolicyMatrixRouteRoute '/reconnect': typeof ReconnectRouteRoute '/serving': typeof ServingRouteRoute @@ -135,6 +142,7 @@ export interface FileRoutesByTo { '/genie': typeof GenieRouteRoute '/jobs': typeof JobsRouteRoute '/lakebase': typeof LakebaseRouteRoute + '/metrics': typeof MetricsRouteRoute '/policy-matrix': typeof PolicyMatrixRouteRoute '/reconnect': typeof ReconnectRouteRoute '/serving': typeof ServingRouteRoute @@ -154,6 +162,7 @@ export interface FileRoutesById { '/genie': typeof GenieRouteRoute '/jobs': typeof JobsRouteRoute '/lakebase': typeof LakebaseRouteRoute + '/metrics': typeof MetricsRouteRoute '/policy-matrix': typeof PolicyMatrixRouteRoute '/reconnect': typeof ReconnectRouteRoute '/serving': typeof ServingRouteRoute @@ -174,6 +183,7 @@ export interface FileRouteTypes { | '/genie' | '/jobs' | '/lakebase' + | '/metrics' | '/policy-matrix' | '/reconnect' | '/serving' @@ -192,6 +202,7 @@ export interface FileRouteTypes { | '/genie' | '/jobs' | '/lakebase' + | '/metrics' | '/policy-matrix' | '/reconnect' | '/serving' @@ -210,6 +221,7 @@ export interface FileRouteTypes { | '/genie' | '/jobs' | '/lakebase' + | '/metrics' | '/policy-matrix' | '/reconnect' | '/serving' @@ -229,6 +241,7 @@ export interface RootRouteChildren { GenieRouteRoute: typeof GenieRouteRoute JobsRouteRoute: typeof JobsRouteRoute LakebaseRouteRoute: typeof LakebaseRouteRoute + MetricsRouteRoute: typeof MetricsRouteRoute PolicyMatrixRouteRoute: typeof PolicyMatrixRouteRoute ReconnectRouteRoute: typeof ReconnectRouteRoute ServingRouteRoute: typeof ServingRouteRoute @@ -289,6 +302,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PolicyMatrixRouteRouteImport parentRoute: typeof rootRouteImport } + '/metrics': { + id: '/metrics' + path: '/metrics' + fullPath: '/metrics' + preLoaderRoute: typeof MetricsRouteRouteImport + parentRoute: typeof rootRouteImport + } '/lakebase': { id: '/lakebase' path: '/lakebase' @@ -365,6 +385,7 @@ const rootRouteChildren: RootRouteChildren = { GenieRouteRoute: GenieRouteRoute, JobsRouteRoute: JobsRouteRoute, LakebaseRouteRoute: LakebaseRouteRoute, + MetricsRouteRoute: MetricsRouteRoute, PolicyMatrixRouteRoute: PolicyMatrixRouteRoute, ReconnectRouteRoute: ReconnectRouteRoute, ServingRouteRoute: ServingRouteRoute, diff --git a/apps/dev-playground/client/src/routes/__root.tsx b/apps/dev-playground/client/src/routes/__root.tsx index db42fdafb..ea7659ef2 100644 --- a/apps/dev-playground/client/src/routes/__root.tsx +++ b/apps/dev-playground/client/src/routes/__root.tsx @@ -40,6 +40,14 @@ function RootComponent() { Analytics + + + + + +

diff --git a/apps/dev-playground/client/src/routes/metrics.route.tsx b/apps/dev-playground/client/src/routes/metrics.route.tsx new file mode 100644 index 000000000..73ea56862 --- /dev/null +++ b/apps/dev-playground/client/src/routes/metrics.route.tsx @@ -0,0 +1,364 @@ +import { + formatLabel, + formatValue, + toD3Format, +} from "@databricks/appkit-ui/format"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + useMetricView, +} from "@databricks/appkit-ui/react"; +import { createFileRoute } from "@tanstack/react-router"; +import { useEffect, useMemo, useState } from "react"; +import Plot from "react-plotly.js"; +import { Header } from "@/components/layout/header"; + +export const Route = createFileRoute("/metrics")({ + component: MetricsRoute, +}); + +/** + * Phase 7 demo route — exercises the full UC Metric View stack: + * + * 1. `useMetricView("revenue", { measures, dimensions, timeGrain, filter })` + * against the SP-lane revenue metric. Plotly chart wires `metadata.measures.arr.format` + * into `layout.yaxis.tickformat` via `toD3Format()` and `metadata.measures.arr.display_name` + * into the trace name + axis title via `formatLabel()`. + * + * 2. `useMetricView("customer_metrics", ...)` against the OBO-lane customer metric. + * The dev-playground exposes `/whoami` so the route can show "executing as ". + * Cache keys for OBO entries incorporate the hashed user identity (Phase 4), + * so the SP and OBO panels live independently. + * + * 3. A hardcoded structured filter (`region in [EMEA, APAC]`) demonstrates the + * 12-op filter spec — the server validates the predicate and parameterizes + * the values before they reach the SQL Warehouse. + * + * Graceful degradation: the demo workspace does not host the underlying UC + * metric views, so both queries surface a server error in real dev sessions. + * The route renders the metadata flow + the typed surface either way; the + * "Could not load metric" panel is the v1 demo's expected behavior. + */ + +interface WhoamiResponse { + xForwardedUser: string | null; + adminUserId: string | null; + isAdmin: boolean; +} + +function useWhoami() { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let cancelled = false; + fetch("/whoami") + .then((res) => res.json() as Promise) + .then((data) => { + if (cancelled) return; + setUser(data.xForwardedUser); + setLoading(false); + }) + .catch(() => { + if (!cancelled) setLoading(false); + }); + return () => { + cancelled = true; + }; + }, []); + + return { user, loading }; +} + +/** + * Plotly trace data shape for the revenue line chart. Built from the result of + * `useMetricView("revenue", ...)`. Each row carries the chosen measure (`arr`) + * and the chosen dimensions (`region`, `created_at`). + */ +type RevenueRow = { + arr: number; + region: string; + created_at: string; +}; + +function RevenueChart() { + // Wrap args in `useMemo` so reference stability prevents infinite refetches. + const args = useMemo( + () => + ({ + measures: ["arr"] as const, + dimensions: ["region", "created_at"] as const, + timeGrain: "month" as const, + filter: { + member: "region", + operator: "in", + values: ["EMEA", "APAC", "AMER"], + }, + }) as const, + [], + ); + + const { data, metadata, loading, error } = useMetricView("revenue", args); + + if (loading) { + return ( +
+ Loading revenue metric… +
+ ); + } + + if (error) { + return ( +
+

Could not load the revenue metric.

+

+ The dev workspace does not host the demo metric view at{" "} + appkit_demo.public.revenue_metrics. + The typed surface and metadata flow still compile — this panel would + render a Plotly line chart with{" "} + $#,##0.00 tick formatting once the + metric view exists in your warehouse. +

+

Server error: {error}

+
+ ); + } + + if (!data || data.length === 0) { + return ( +
+ No rows returned. +
+ ); + } + + // Group by region — one Plotly trace per series. + const rows = data as RevenueRow[]; + const byRegion = new Map(); + for (const row of rows) { + if (!byRegion.has(row.region)) { + byRegion.set(row.region, { x: [], y: [] }); + } + const entry = byRegion.get(row.region); + if (!entry) continue; + entry.x.push(row.created_at); + entry.y.push(row.arr); + } + + const traces = Array.from(byRegion.entries()).map(([region, series]) => ({ + type: "scatter" as const, + mode: "lines+markers" as const, + name: region, + x: series.x, + y: series.y, + hovertemplate: `${region}
%{x|%b %Y}
%{y}`, + })); + + // Wire metadata into Plotly layout. `formatLabel` returns the YAML-defined + // display name; `toD3Format` converts the YAML's printf-style format spec + // into the d3-format syntax that Plotly's `tickformat` understands. + const arrLabel = formatLabel("arr", metadata?.measures.arr); + const arrTickFormat = toD3Format(metadata?.measures.arr.format); + + return ( + + ); +} + +function CustomerMetricsPanel({ user }: { user: string | null }) { + const args = useMemo( + () => + ({ + measures: ["active_accounts", "churn_rate"] as const, + dimensions: ["segment"] as const, + }) as const, + [], + ); + + const { data, metadata, loading, error } = useMetricView( + "customer_metrics", + args, + ); + + return ( +
+

+ Executing as{" "} + + {user ?? ""} + + . OBO entries scope cache keys per user — different users see different + rows, even with identical args. +

+ {loading && ( +
+ Loading customer metrics… +
+ )} + {error && ( +
+

Could not load customer metrics.

+

+ The dev workspace does not host the demo metric view at{" "} + appkit_demo.public.customer_metrics + . When wired to a real OBO-lane metric view, this panel would show + row-level scoping driven by{" "} + x-forwarded-access-token. +

+

Server error: {error}

+
+ )} + {data && data.length > 0 && ( + + + + + + + + + + {( + data as Array<{ + segment: string; + active_accounts: number; + churn_rate: number; + }> + ).map((row) => ( + + + + + + ))} + +
+ {formatLabel("segment", metadata?.dimensions.segment)} + + {formatLabel( + "active_accounts", + metadata?.measures.active_accounts, + )} + + {formatLabel("churn_rate", metadata?.measures.churn_rate)} +
{row.segment} + {formatValue( + row.active_accounts, + metadata?.measures.active_accounts.format, + )} + + {formatValue( + row.churn_rate, + metadata?.measures.churn_rate.format, + )} +
+ )} +
+ ); +} + +function MetricsRoute() { + const { user } = useWhoami(); + + return ( +
+
+
+ +
+ + + Revenue (SP lane) + + Annual Recurring Revenue by region, monthly grain. Filter: + region in {`{EMEA, APAC, AMER}`}. The Y-axis tick format and + trace name are sourced from the metric view's YAML metadata via{" "} + toD3Format() and{" "} + formatLabel(). + + + + + + + + + + Customer Metrics (OBO lane) + + Active accounts and churn rate, grouped by segment. OBO entries + scope cache keys per requesting user. + + + + + + + + + + How this demo wires together + + +
    +
  1. + config/queries/metric.json declares two metric + sources — revenue (SP lane) and{" "} + customer_metrics (OBO lane). +
  2. +
  3. + npx appkit metric sync regenerates a typed{" "} + metrics.d.ts (augmenting{" "} + MetricRegistry) and a{" "} + metrics.metadata.json bundle. +
  4. +
  5. + main.tsx imports the metadata bundle once at + startup and calls registerMetricsMetadata(). +
  6. +
  7. + useMetricView("revenue", ...) narrows + measures, dimensions, and time grains to the registry-known + literals — typos fail at compile time. +
  8. +
  9. + formatLabel, formatValue, and{" "} + toD3Format turn the YAML metadata into Plotly / + table-cell strings — no chart-library lock-in. +
  10. +
+
+
+
+
+
+ ); +} diff --git a/apps/dev-playground/client/tsconfig.app.json b/apps/dev-playground/client/tsconfig.app.json index 6e2a3b464..ddf430cdc 100644 --- a/apps/dev-playground/client/tsconfig.app.json +++ b/apps/dev-playground/client/tsconfig.app.json @@ -15,6 +15,7 @@ "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", + "resolveJsonModule": true, /* Linting */ "strict": true, diff --git a/apps/dev-playground/shared/appkit-types/metrics.d.ts b/apps/dev-playground/shared/appkit-types/metrics.d.ts new file mode 100644 index 000000000..c9d41b398 --- /dev/null +++ b/apps/dev-playground/shared/appkit-types/metrics.d.ts @@ -0,0 +1,135 @@ +// Auto-generated by AppKit - DO NOT EDIT +// In a production app this file is regenerated by `npx @databricks/appkit metric sync` +// or the Vite type-generator plugin. Phase 7 ships this hand-authored copy so the +// dev-playground demo at /metrics compiles even when the dev workspace does not +// host the underlying UC metric views — the typed surface is what we want to +// showcase. +import "@databricks/appkit-ui/react"; + +declare module "@databricks/appkit-ui/react" { + interface MetricRegistry { + revenue: { + key: "revenue"; + source: "appkit_demo.public.revenue_metrics"; + lane: "sp"; + measures: { + /** @sqlType DECIMAL(38,2) */ + arr: number; + /** @sqlType DECIMAL(38,2) */ + mrr: number; + /** @sqlType DECIMAL(38,2) */ + new_arr: number; + /** @sqlType DECIMAL(38,2) */ + churned_arr: number; + }; + dimensions: { + /** @sqlType STRING */ + region: string; + /** @sqlType STRING */ + segment: string; + /** @sqlType TIMESTAMP @timeGrain day|week|month|quarter|year */ + created_at: string; + }; + measureKeys: "arr" | "mrr" | "new_arr" | "churned_arr"; + dimensionKeys: "region" | "segment" | "created_at"; + timeGrains: "day" | "week" | "month" | "quarter" | "year"; + metadata: { + measures: { + arr: { + type: "DECIMAL(38,2)"; + display_name: "Annual Recurring Revenue"; + format: "$#,##0.00"; + description: "Annualized contract value across all active subscriptions"; + }; + mrr: { + type: "DECIMAL(38,2)"; + display_name: "Monthly Recurring Revenue"; + format: "$#,##0.00"; + }; + new_arr: { + type: "DECIMAL(38,2)"; + display_name: "New ARR"; + format: "$#,##0.00"; + }; + churned_arr: { + type: "DECIMAL(38,2)"; + display_name: "Churned ARR"; + format: "$#,##0.00"; + }; + }; + dimensions: { + region: { + type: "STRING"; + display_name: "Region"; + }; + segment: { + type: "STRING"; + display_name: "Customer Segment"; + }; + created_at: { + type: "TIMESTAMP"; + display_name: "Subscription Start"; + time_grain: readonly ["day", "week", "month", "quarter", "year"]; + }; + }; + }; + }; + customer_metrics: { + key: "customer_metrics"; + source: "appkit_demo.public.customer_metrics"; + lane: "obo"; + measures: { + /** @sqlType BIGINT */ + active_accounts: number; + /** @sqlType DECIMAL(10,4) */ + churn_rate: number; + /** @sqlType DECIMAL(38,2) */ + avg_ltv: number; + }; + dimensions: { + /** @sqlType STRING */ + segment: string; + /** @sqlType STRING */ + region: string; + /** @sqlType STRING */ + csm_email: string; + }; + measureKeys: "active_accounts" | "churn_rate" | "avg_ltv"; + dimensionKeys: "segment" | "region" | "csm_email"; + timeGrains: never; + metadata: { + measures: { + active_accounts: { + type: "BIGINT"; + display_name: "Active Accounts"; + format: "#,##0"; + }; + churn_rate: { + type: "DECIMAL(10,4)"; + display_name: "Churn Rate"; + format: "0.0%"; + }; + avg_ltv: { + type: "DECIMAL(38,2)"; + display_name: "Average LTV"; + format: "$#,##0.00"; + }; + }; + dimensions: { + segment: { + type: "STRING"; + display_name: "Customer Segment"; + }; + region: { + type: "STRING"; + display_name: "Region"; + }; + csm_email: { + type: "STRING"; + display_name: "CSM Email"; + }; + }; + }; + }; + } +} diff --git a/apps/dev-playground/shared/appkit-types/metrics.metadata.json b/apps/dev-playground/shared/appkit-types/metrics.metadata.json new file mode 100644 index 000000000..87a519b9a --- /dev/null +++ b/apps/dev-playground/shared/appkit-types/metrics.metadata.json @@ -0,0 +1,79 @@ +{ + "revenue": { + "source": "appkit_demo.public.revenue_metrics", + "lane": "sp", + "measures": { + "arr": { + "type": "DECIMAL(38,2)", + "display_name": "Annual Recurring Revenue", + "format": "$#,##0.00", + "description": "Annualized contract value across all active subscriptions" + }, + "mrr": { + "type": "DECIMAL(38,2)", + "display_name": "Monthly Recurring Revenue", + "format": "$#,##0.00" + }, + "new_arr": { + "type": "DECIMAL(38,2)", + "display_name": "New ARR", + "format": "$#,##0.00" + }, + "churned_arr": { + "type": "DECIMAL(38,2)", + "display_name": "Churned ARR", + "format": "$#,##0.00" + } + }, + "dimensions": { + "region": { + "type": "STRING", + "display_name": "Region" + }, + "segment": { + "type": "STRING", + "display_name": "Customer Segment" + }, + "created_at": { + "type": "TIMESTAMP", + "display_name": "Subscription Start", + "time_grain": ["day", "week", "month", "quarter", "year"] + } + } + }, + "customer_metrics": { + "source": "appkit_demo.public.customer_metrics", + "lane": "obo", + "measures": { + "active_accounts": { + "type": "BIGINT", + "display_name": "Active Accounts", + "format": "#,##0" + }, + "churn_rate": { + "type": "DECIMAL(10,4)", + "display_name": "Churn Rate", + "format": "0.0%" + }, + "avg_ltv": { + "type": "DECIMAL(38,2)", + "display_name": "Average LTV", + "format": "$#,##0.00" + } + }, + "dimensions": { + "segment": { + "type": "STRING", + "display_name": "Customer Segment" + }, + "region": { + "type": "STRING", + "display_name": "Region" + }, + "csm_email": { + "type": "STRING", + "display_name": "CSM Email" + } + } + } +} diff --git a/apps/dev-playground/tests/metrics.spec.ts b/apps/dev-playground/tests/metrics.spec.ts new file mode 100644 index 000000000..fa974784c --- /dev/null +++ b/apps/dev-playground/tests/metrics.spec.ts @@ -0,0 +1,159 @@ +import { expect, test } from "@playwright/test"; + +/** + * Phase 7 acceptance test for the `/metrics` demo route. Exercises the full + * metric-view path through dev mode for one happy-path case (revenue, SP lane + * with metadata flow) and one error case (customer_metrics, OBO lane returns + * 404 because the demo workspace does not host the metric view). + * + * The mocks bypass the real SQL Warehouse so the test does not require live + * Databricks credentials. SSE response envelopes match the existing analytics + * route's shape (`{ type: "result", data }`). + */ + +const SSE_HEADERS = { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", +}; + +function sseEvent(payload: unknown): string { + return `data: ${JSON.stringify(payload)}\n\n`; +} + +const REVENUE_ROWS = [ + { region: "EMEA", created_at: "2026-01-01T00:00:00Z", arr: 1_250_000 }, + { region: "EMEA", created_at: "2026-02-01T00:00:00Z", arr: 1_310_000 }, + { region: "APAC", created_at: "2026-01-01T00:00:00Z", arr: 720_000 }, + { region: "APAC", created_at: "2026-02-01T00:00:00Z", arr: 760_000 }, + { region: "AMER", created_at: "2026-01-01T00:00:00Z", arr: 2_400_000 }, + { region: "AMER", created_at: "2026-02-01T00:00:00Z", arr: 2_470_000 }, +]; + +test.describe("Metric Views Route Tests", () => { + test.beforeEach(async ({ page }) => { + // Happy-path mock: revenue (SP lane) returns six rows. + await page.route("**/api/analytics/metric/revenue", async (route) => { + // Best-effort: confirm the request body carries the expected + // measures/dimensions/timeGrain/filter shape — the demo's call site is + // contractual. + const body = route.request().postDataJSON(); + expect(body).toMatchObject({ + measures: ["arr"], + dimensions: ["region", "created_at"], + timeGrain: "month", + filter: { + member: "region", + operator: "in", + values: ["EMEA", "APAC", "AMER"], + }, + }); + + return route.fulfill({ + status: 200, + headers: SSE_HEADERS, + body: sseEvent({ type: "result", data: REVENUE_ROWS }), + }); + }); + + // Error-path mock: customer_metrics (OBO lane) returns a 404-shaped error + // event. Mirrors the experience when the dev workspace does not host the + // OBO metric view. + await page.route( + "**/api/analytics/metric/customer_metrics", + async (route) => { + return route.fulfill({ + status: 200, + headers: SSE_HEADERS, + body: sseEvent({ + type: "error", + error: "Metric view not found", + code: "METRIC_NOT_FOUND", + }), + }); + }, + ); + + // /whoami stub — the OBO panel surfaces the user identity. + await page.route("**/whoami", async (route) => { + return route.fulfill({ + json: { + xForwardedUser: "demo-user@databricks.com", + adminUserId: null, + isAdmin: false, + }, + }); + }); + }); + + test("metrics page loads and renders the route header", async ({ page }) => { + await page.goto("/metrics", { waitUntil: "networkidle" }); + + await expect(page).toHaveURL("/metrics"); + await expect( + page.getByRole("heading", { name: "Metric Views" }), + ).toBeVisible(); + }); + + test("revenue chart renders with metadata-formatted axis", async ({ + page, + }) => { + await page.goto("/metrics", { waitUntil: "networkidle" }); + + // Plotly renders an SVG inside `.js-plotly-plot`; its presence is the + // load-bearing assertion that the metric query resolved + the metadata + // flowed into the chart layout. + const plotContainer = page.locator(".js-plotly-plot").first(); + await expect(plotContainer).toBeVisible({ timeout: 10000 }); + + // The Y-axis title comes from `formatLabel("arr", metadata.measures.arr)` + // — the metadata's `display_name` field. Two instances appear (chart title + // + Y-axis title), so we assert at least one. + await expect( + page.getByText("Annual Recurring Revenue").first(), + ).toBeVisible(); + }); + + test("OBO panel shows the requesting user identity", async ({ page }) => { + await page.goto("/metrics", { waitUntil: "networkidle" }); + + // The /whoami response surfaces the mock user; the OBO panel exposes it. + await expect(page.getByText("demo-user@databricks.com")).toBeVisible(); + }); + + test("OBO error path renders the graceful fallback banner", async ({ + page, + }) => { + await page.goto("/metrics", { waitUntil: "networkidle" }); + + // The error mock returns `code: "METRIC_NOT_FOUND"`. The route renders + // a banner with the literal "Could not load customer metrics." message + // when the OBO query fails — the v1 demo's expected fallback. + await expect( + page.getByText("Could not load customer metrics."), + ).toBeVisible({ timeout: 10000 }); + }); + + test("calls expected metric endpoints on page load", async ({ page }) => { + const calls: string[] = []; + page.on("request", (request) => { + if (request.url().includes("/api/analytics/metric/")) { + calls.push(request.url()); + } + }); + + await page.goto("/metrics", { waitUntil: "networkidle" }); + + // React 19 Strict Mode doubles useEffect invocations in dev mode; assert + // both routes fire (allowing for the multiplier). + const revenueCalls = calls.filter((u) => + u.endsWith("/api/analytics/metric/revenue"), + ); + const customerCalls = calls.filter((u) => + u.endsWith("/api/analytics/metric/customer_metrics"), + ); + + expect(revenueCalls.length).toBeGreaterThanOrEqual(1); + expect(customerCalls.length).toBeGreaterThanOrEqual(1); + }); +}); diff --git a/docs/docs/plugins/analytics-metric-views.md b/docs/docs/plugins/analytics-metric-views.md new file mode 100644 index 000000000..eb7fc73d3 --- /dev/null +++ b/docs/docs/plugins/analytics-metric-views.md @@ -0,0 +1,585 @@ +--- +sidebar_position: 4 +--- + +# Analytics — Metric Views + +UC Metric View consumption built on top of the analytics plugin: declarative metric registration, a typed React hook, structured filters, time-grain truncation, and library-agnostic format utilities. + +**Key features:** +- Declarative `metric.json` config with `sp` and `obo` execution lanes +- `useMetricView` React hook with measure/dimension narrowing at the call site +- Structured filter spec — 12 operators, AND/OR composition, schema-validated members +- Time-grain truncation on time-typed dimensions +- Build-time semantic metadata bundle + library-agnostic format utilities +- `npx appkit metric sync` CLI for non-Vite builds, CI checks, pre-commit hooks +- OBO row scoping with cross-user cache isolation + +The metric-view path lives inside the existing analytics plugin — apps without `metric.json` pay no bundle or runtime cost. See the [Analytics plugin](./analytics.md) for the underlying SQL execution machinery. + +## Configuration: `metric.json` + +Place a `metric.json` file alongside your `.sql` query files: + +```json title="config/queries/metric.json" +{ + "$schema": "https://databricks.github.io/appkit/schemas/metric-source.schema.json", + "sp": { + "revenue": { + "source": "appkit_demo.public.revenue_metrics" + } + }, + "obo": { + "customer_metrics": { + "source": "appkit_demo.public.customer_metrics" + } + } +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `sp` | `Record` | Metrics executed as the service principal — shared cache. | +| `obo` | `Record` | Metrics executed on-behalf-of the requesting user — per-user cache. | +| `.source` | `string` | Three-part Unity Catalog FQN of the metric view (`..`). | + +The map key (`revenue`, `customer_metrics`) is the **single identity** that flows through every other surface: the route key in `POST /api/analytics/metric/:key`, the hook argument in `useMetricView("", ...)`, the `MetricRegistry` augmentation key, and the cache-key segment. + +The entry-object shape (`{ source }` at v1) is the forward-compat seam — future per-entry options (`cacheTtl`, `defaultFilter`, allowlists) grow non-breakingly. v1 deliberately rejects unknown fields. + +The [JSON Schema](https://databricks.github.io/appkit/schemas/metric-source.schema.json) ships with AppKit; configure your IDE to validate `metric.json` against it. + +## HTTP endpoint + +The analytics plugin exposes one new endpoint (mounted under `/api/analytics`): + +- `POST /api/analytics/metric/:key` + +The Arrow secondary path (`GET /api/analytics/arrow-result/:jobId`) is reused unchanged. + +### Request body + +```ts +{ + measures: string[]; // Required. Subset of declared measures. + dimensions?: string[]; // Optional. Subset of declared dimensions. + filter?: Filter; // Optional. Recursive AND/OR/Predicate tree. + timeGrain?: string; // Optional. Applies to time-typed dimensions. + limit?: number; // Optional. Row cap. + format?: "JSON"; // Optional. ARROW deferred to a future release. +} +``` + +### Response envelope + +The route emits the same SSE event shape as `/api/analytics/query/:query_key`: + +| Event | Description | +|-------|-------------| +| `result` | Final result payload (JSON rows). | +| `arrow` | Reserved — ARROW format is out of scope at v1. | +| `error` | Error event with `code` + `message`. | +| `warning` | Non-fatal advisory (e.g., row cap applied). | + +## Frontend usage + +### `useMetricView` + +```ts +import { useMetricView } from "@databricks/appkit-ui/react"; + +const { data, metadata, loading, error } = useMetricView(metricKey, args, options); +``` + +Signature: + +```ts +function useMetricView< + K extends MetricKey, + M extends ReadonlyArray>, + D extends ReadonlyArray>, + F extends AnalyticsFormat = "JSON", +>( + metricKey: K, + args: { + measures: M; + dimensions?: D; + filter?: Filter; + timeGrain?: TimeGrain; + limit?: number; + }, + options?: { + format?: F; + autoStart?: boolean; + maxParametersSize?: number; + }, +): { + data: Pick, M[number] | D[number]>[] | null; + metadata: MetricMetadata | null; + loading: boolean; + error: string | null; +}; +``` + +**Generic narrowing:** +- `K` narrows to a registered metric key when `MetricRegistry` is augmented. +- `M` and `D` carry `const` modifiers — pass `as const` on the arrays to preserve literal types. +- The result row type is `Pick, M[number] | D[number]>` — the IDE shows exactly the columns you projected. + +**Return shape:** + +| Field | Type | Description | +|-------|------|-------------| +| `data` | Row array \| `null` | Picked-down rows once the query completes. | +| `metadata` | `MetricMetadata` \| `null` | Build-time metadata for the queried metric (measures + dimensions). Available **before** `data` loads; stable across re-renders. | +| `loading` | `boolean` | `true` while the request is in flight. | +| `error` | `string \| null` | Error message; `null` on success. | + +**Options:** + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `format` | `"JSON"` | `"JSON"` | Response format. ARROW deferred. | +| `autoStart` | `boolean` | `true` | Fire the request on mount. | +| `maxParametersSize` | `number` | `102400` | Max serialized request body size in bytes. | + +**Example:** + +```tsx +import { useMetricView } from "@databricks/appkit-ui/react"; +import { useMemo } from "react"; + +function RevenueChart() { + const args = useMemo( + () => + ({ + measures: ["arr"] as const, + dimensions: ["region", "created_at"] as const, + timeGrain: "month" as const, + }) as const, + [], + ); + + const { data, metadata, loading, error } = useMetricView("revenue", args); + + if (loading) return ; + if (error) return ; + if (!data?.length) return ; + + // data: Array<{ arr: number; region: string; created_at: string }> + // metadata.measures.arr.format → "$#,##0.00" + // metadata.measures.arr.display_name → "Annual Recurring Revenue" + return ; +} +``` + +:::tip Memoize args + +Wrap the `args` object in `useMemo` so reference stability prevents infinite refetches — the hook re-fires whenever the args reference changes, mirroring `useAnalyticsQuery`. +::: + +### Type-safe registration + +The build-time pipeline augments the `MetricRegistry` interface declared in `@databricks/appkit-ui/react`. The generated `metrics.d.ts` looks like: + +```ts +declare module "@databricks/appkit-ui/react" { + interface MetricRegistry { + revenue: { + key: "revenue"; + source: "appkit_demo.public.revenue_metrics"; + lane: "sp"; + measures: { arr: number; mrr: number }; + dimensions: { region: string; created_at: string }; + measureKeys: "arr" | "mrr"; + dimensionKeys: "region" | "created_at"; + timeGrains: "day" | "week" | "month"; + metadata: { + measures: { + arr: { + type: "DECIMAL(38,2)"; + display_name: "Annual Recurring Revenue"; + format: "$#,##0.00"; + }; + }; + dimensions: { + region: { type: "STRING" }; + created_at: { + type: "TIMESTAMP"; + time_grain: readonly ["day", "week", "month"]; + }; + }; + }; + }; + } +} +``` + +Once augmented, `useMetricView("revenue", { measures: ["arr"] })` autocompletes measure names, rejects typos at compile time, and narrows the result row type at the call site. + +## Filter spec + +The structured filter is a recursive type: + +```ts +type Filter = + | Predicate + | { and: ReadonlyArray> } + | { or: ReadonlyArray> }; + +interface Predicate { + member: DimensionKey; + operator: MetricFilterOperator; + values?: ReadonlyArray; +} +``` + +The 12 v1 operators: + +| Category | Operators | Cardinality | Notes | +|----------|-----------|-------------|-------| +| Equality | `equals`, `notEquals` | exactly one value | Any dimension type. | +| Set membership | `in`, `notIn` | one or more values | Any dimension type. | +| Range | `gt`, `gte`, `lt`, `lte` | exactly one value | Numeric / date-typed dimensions only. | +| String search | `contains`, `notContains` | exactly one value | String-typed dimensions only. | +| NULL checks | `set`, `notSet` | no values (rejected if present) | Any dimension type. | + +`startsWith`, `endsWith`, `between`, and date-range helpers are reserved for v1.5. + +### Examples + +**Single predicate:** + +```ts +filter: { member: "region", operator: "in", values: ["EMEA", "APAC"] } +``` + +**Implicit AND (predicate list inside an `and` group):** + +```ts +filter: { + and: [ + { member: "region", operator: "in", values: ["EMEA"] }, + { member: "segment", operator: "equals", values: ["Enterprise"] }, + ], +} +``` + +**Nested OR:** + +```ts +filter: { + or: [ + { + and: [ + { member: "region", operator: "equals", values: ["EMEA"] }, + { member: "segment", operator: "equals", values: ["Enterprise"] }, + ], + }, + { member: "region", operator: "equals", values: ["APAC"] }, + ], +} +``` + +**NULL check:** + +```ts +filter: { member: "csm_email", operator: "set" } +``` + +The server enforces: +- `member` is a registered dimension on the metric (typos return 400). +- `operator` is one of the 12 v1 names. +- Operator-vs-type compatibility (e.g., `gt` on a string dimension returns 400). +- `values` cardinality matches the operator. +- Recursion depth ≤ 8 (defense against malformed payloads). +- All values bind as parameters via the Statement Execution bind-var path — **no value from the request body flows into the rendered SQL string**. + +## Time grain + +`timeGrain` is a single optional top-level field on the request body. When set, it applies to every time-typed dimension in `dimensions`: + +```ts +useMetricView("revenue", { + measures: ["arr"] as const, + dimensions: ["created_at"] as const, + timeGrain: "month", +}); +``` + +Generated SQL: + +```sql +SELECT date_trunc('month', created_at) AS created_at, MEASURE(arr) AS arr +FROM appkit_demo.public.revenue_metrics +GROUP BY ALL +``` + +The `TimeGrain` type narrows to the union of grains the metric view's YAML 1.1 `time_grain` attribute declares. Setting `timeGrain` without including a time-typed dimension in `dimensions` returns 400 with `timeGrain specified but no time-typed dimension grouped`. + +Date ranges are expressed via the structured filter spec (`gte`/`lte` predicates on the time dimension), not a separate `dateRange` field. + +## Semantic metadata + format utilities + +Build-time, the type-generator emits `metrics.metadata.json` alongside the typed `.d.ts`: + +```json +{ + "revenue": { + "source": "appkit_demo.public.revenue_metrics", + "lane": "sp", + "measures": { + "arr": { + "type": "DECIMAL(38,2)", + "display_name": "Annual Recurring Revenue", + "format": "$#,##0.00", + "description": "Annualized contract value across active subscriptions" + } + }, + "dimensions": { + "created_at": { + "type": "TIMESTAMP", + "display_name": "Subscription Start", + "time_grain": ["day", "week", "month"] + } + } + } +} +``` + +Register the bundle once at app startup: + +```ts title="src/main.tsx" +import { registerMetricsMetadata } from "@databricks/appkit-ui/format"; +import metricsMetadata from "../shared/appkit-types/metrics.metadata.json"; + +registerMetricsMetadata(metricsMetadata); +``` + +`useMetricView` then returns the relevant subset (measures + dimensions for the queried metric) in `metadata`. The reference is stable across re-renders for the same metric key. + +### Library-agnostic format utilities + +Three pure functions in `@databricks/appkit-ui/format`: + +```ts +import { formatLabel, formatValue, toD3Format } from "@databricks/appkit-ui/format"; +``` + +| Function | Signature | Purpose | +|----------|-----------|---------| +| `formatValue(value, format?)` | `(value, format?) => string` | Turns a raw value + UC format spec into a display string. | +| `formatLabel(name, columnMetadata?)` | `(name, columnMetadata?) => string` | Returns `display_name` or humanizes the column name. | +| `toD3Format(format?)` | `(format?) => string` | Converts a UC printf-style spec to a d3-format-compatible string. | + +Recognized format specs (passthrough — UC's YAML 1.1 emits printf-style strings, AppKit forwards them): + +| YAML format | `formatValue(1234.56, ...)` | `toD3Format(...)` | +|-------------|----------------------------|-------------------| +| `$#,##0.00` | `"$1,234.56"` | `"$,.2f"` | +| `0.00%` | `"123,456.00%"` (use `0.0%` for ratios) | `".2%"` | +| `0.0%` | `"42.7%"` (input `0.427`) | `".1%"` | +| `#,##0` | `"1,235"` | `",.0f"` | +| `0.000` | `"1234.560"` | `".3f"` | +| (omitted) | localized number formatting | `""` (let chart use defaults) | + +### Plotly example + +```tsx +import { formatLabel, toD3Format } from "@databricks/appkit-ui/format"; +import { useMetricView } from "@databricks/appkit-ui/react"; +import Plot from "react-plotly.js"; + +function ARRChart() { + const { data, metadata } = useMetricView("revenue", { + measures: ["arr"] as const, + dimensions: ["created_at"] as const, + timeGrain: "month", + }); + + if (!data || !metadata) return null; + + return ( + row.created_at), + y: data.map((row) => row.arr), + }, + ]} + layout={{ + title: { text: formatLabel("arr", metadata.measures.arr) }, + yaxis: { tickformat: toD3Format(metadata.measures.arr.format) }, + }} + /> + ); +} +``` + +### ECharts example + +```tsx +import { formatLabel, toD3Format } from "@databricks/appkit-ui/format"; +import { useMetricView } from "@databricks/appkit-ui/react"; +import ReactECharts from "echarts-for-react"; + +function ARRChart() { + const { data, metadata } = useMetricView("revenue", { + measures: ["arr"] as const, + dimensions: ["created_at"] as const, + timeGrain: "month", + }); + + if (!data || !metadata) return null; + + return ( + r.created_at) }, + yAxis: { + type: "value", + axisLabel: { + // ECharts accepts a d3-format-compatible string via formatter, + // or a function form for full control. + formatter: toD3Format(metadata.measures.arr.format), + }, + }, + series: [ + { + type: "line", + data: data.map((r) => r.arr), + name: formatLabel("arr", metadata.measures.arr), + }, + ], + }} + /> + ); +} +``` + +The format utilities are deliberately library-agnostic — they emit strings the consumer's chart library decides how to consume. Wrapping a specific chart-library API is glue customers can write in tens of lines, not the framework's responsibility. + +## CLI + +```bash +npx appkit metric sync +``` + +The `metric sync` subcommand calls the same `syncMetrics()` core that the Vite type-generator runs in dev mode. Useful for: + +- CI checks (verify generated types are committed and match the warehouse). +- Non-Vite builds (Webpack, Rspack, Turbopack, raw `tsc`). +- Manual refresh after a teammate's `metric.json` change. +- Pre-commit hooks. + +Flags: + +| Flag | Description | +|------|-------------| +| `--warehouse-id ` | Override the default warehouse. | +| `--metric-json-path ` | Override the default `config/queries/metric.json` location. | +| `--output-dir ` | Override where the generated `metrics.d.ts` and `metrics.metadata.json` land. | +| `--silent` | Suppress non-error output. | + +The CLI exits with: +- `0` on success. +- Non-zero with a recognizable message for missing FQN, unreachable warehouse, malformed `metric.json`, or schema-fetch authentication failure. + +Future subcommands (`list`, `validate`, `describe`) plug into the same parent command. + +## Security model + +The metric-view path inherits AppKit's plugin-best-practices defaults and adds a few metric-specific reinforcements: + +1. **Validator-first.** Every column name (`measures`, `dimensions`, filter `member`) is checked against the build-time schema snapshot before SQL construction. **No user-supplied string is ever interpolated into the generated SQL.** Unknown columns return 400. + +2. **Operator allowlist.** The 12 v1 operator names are an exhaustive enum — any other string in `operator` returns 400. + +3. **Operator-vs-type compatibility.** `gt` on a string dim returns 400. `contains` on a numeric dim returns 400. The validator is the source of truth. + +4. **Parameterized values.** Every value in a predicate is bound as a parameter via the Statement Execution bind-var path. SQL injection via filter values is structurally impossible. + +5. **Recursion depth cap.** AND/OR nesting is limited to 8 levels — defense against stack-abuse via hostile payloads. + +6. **OBO row scoping.** Entries in the `obo` lane dispatch via the `asUser(req)` Proxy, threading the user's `x-forwarded-access-token` through every Databricks call. The warehouse executes the query under the end-user's identity. + +7. **Cross-user cache isolation.** OBO cache keys take the form `metric:{key}:{argsHash}:{sha256(userIdentity)}`. The raw email/principal name never reaches the cache layer. SP-lane keys use literal `"sp"` as the executor key — shared cache by design. + +8. **Sort-before-hash on order-insensitive args.** Measures, dimensions, and filter predicates within each AND/OR group are stable-sorted before hashing, so semantically equivalent calls collapse to the same cache entry. + +The server emits four metric-specific telemetry spans: `analytics.metric.query`, `analytics.metric.validate`, `analytics.metric.cache.hit`, `analytics.metric.cache.miss`. Metrics: `metric_query_duration_seconds`, `metric_cache_hit_ratio`, `metric_validation_failures_total`. + +## Migration from hand-rolled metric SQL + +If you previously consumed metric views by hand-writing SQL with `MEASURE(...)` in `.sql` files: + +```sql title="config/queries/revenue.sql (legacy approach)" +SELECT + date_trunc(:grain, created_at) AS created_at, + region, + MEASURE(arr) AS arr +FROM appkit_demo.public.revenue_metrics +WHERE region IN (:r1, :r2) +GROUP BY ALL +``` + +Migrate by: + +1. **Move the FQN into `metric.json`** under `sp` or `obo`: + + ```json title="config/queries/metric.json" + { + "sp": { + "revenue": { "source": "appkit_demo.public.revenue_metrics" } + } + } + ``` + +2. **Replace `useAnalyticsQuery` with `useMetricView`** at the call site: + + ```tsx + // Before + const { data } = useAnalyticsQuery("revenue", { + grain: sql.string("month"), + r1: sql.string("EMEA"), + r2: sql.string("APAC"), + }); + + // After + const { data, metadata } = useMetricView("revenue", { + measures: ["arr"] as const, + dimensions: ["region", "created_at"] as const, + timeGrain: "month", + filter: { + member: "region", + operator: "in", + values: ["EMEA", "APAC"], + }, + }); + ``` + +3. **Delete the `.sql` file.** The server constructs SQL deterministically from the structured args. + +4. **Run `npx appkit metric sync`** (or rely on the Vite plugin) to regenerate `metrics.d.ts` and `metrics.metadata.json`. The `MetricRegistry` augmentation lights up call-site narrowing. + +5. **Optional: wire metadata into your chart.** Use `formatLabel` / `formatValue` / `toD3Format` to consume the YAML's `display_name` and `format` instead of re-typing them in TypeScript. + +The metric-view path is purely additive — your other `.sql` files keep working unchanged. Apps that don't use metric views never load `useMetricView` or the format utilities. + +## Out of scope at v1 + +- **ARROW format.** v1 is JSON-only; metric-view results are typically aggregated and small. +- **Per-entry growth options** (`cacheTtl`, `defaultFilter`, `dimensions` allowlist). +- **Filter ops beyond v1** (`startsWith`, `endsWith`, `between`, date-range family). +- **HAVING (filtering on measures).** v1 restricts `member` to dimensions. +- **Runtime schema refresh.** Build-time only; deploys reset the snapshot. +- **Metric view CRUD.** Read-only consumption at v1. +- **Auto-discovery from UC.** Explicit declaration in `metric.json` is required. +- **Multi-view joins.** One query targets one metric view. +- **Chart-library adapters.** Format utilities are the framework's contribution; chart wrapping is glue customers write in tens of lines. + +Each of these is a non-breaking additive change when concrete demand arrives. diff --git a/docs/static/appkit-ui/styles.gen.css b/docs/static/appkit-ui/styles.gen.css index a2192039d..60da8251b 100644 --- a/docs/static/appkit-ui/styles.gen.css +++ b/docs/static/appkit-ui/styles.gen.css @@ -219,6 +219,9 @@ .pointer-events-none { pointer-events: none; } + .collapse { + visibility: collapse; + } .invisible { visibility: hidden; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 684f6e2e4..4c301f37d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -167,6 +167,124 @@ importers: specifier: npm:rolldown-vite@7.1.14 version: rolldown-vite@7.1.14(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2) + apps/dev-playground/client: + dependencies: + '@radix-ui/react-dropdown-menu': + specifier: 2.1.16 + version: 2.1.16(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-select': + specifier: 2.2.6 + version: 2.2.6(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-slot': + specifier: 1.2.3 + version: 1.2.3(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-tooltip': + specifier: 1.2.8 + version: 1.2.8(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@tanstack/react-router': + specifier: 1.133.22 + version: 1.133.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@tanstack/react-router-devtools': + specifier: 1.133.22 + version: 1.133.22(@tanstack/react-router@1.133.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.133.20)(@types/node@24.6.0)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.12)(terser@5.44.1)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.2) + '@tanstack/react-table': + specifier: 8.21.3 + version: 8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@tanstack/router-plugin': + specifier: 1.133.22 + version: 1.133.22(@tanstack/react-router@1.133.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(rolldown-vite@7.1.14(@types/node@24.6.0)(esbuild@0.25.10)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2))(webpack@5.103.0(esbuild@0.25.10)) + class-variance-authority: + specifier: 0.7.1 + version: 0.7.1 + clsx: + specifier: 2.1.1 + version: 2.1.1 + lucide-react: + specifier: 0.546.0 + version: 0.546.0(react@19.2.0) + plotly.js: + specifier: ^3.5.0 + version: 3.5.0(mapbox-gl@1.13.3) + react: + specifier: 19.2.0 + version: 19.2.0 + react-dom: + specifier: 19.2.0 + version: 19.2.0(react@19.2.0) + react-plotly.js: + specifier: ^2.6.0 + version: 2.6.0(plotly.js@3.5.0(mapbox-gl@1.13.3))(react@19.2.0) + recharts: + specifier: 3.4.1 + version: 3.4.1(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react-is@18.3.1)(react@19.2.0)(redux@5.0.1) + tailwind-merge: + specifier: 3.3.1 + version: 3.3.1 + tailwindcss-animate: + specifier: 1.0.7 + version: 1.0.7(tailwindcss@4.1.17) + tw-animate-css: + specifier: 1.4.0 + version: 1.4.0 + devDependencies: + '@eslint/js': + specifier: 9.36.0 + version: 9.36.0 + '@tailwindcss/postcss': + specifier: 4.1.17 + version: 4.1.17 + '@tanstack/router-cli': + specifier: 1.133.20 + version: 1.133.20 + '@types/node': + specifier: 24.6.0 + version: 24.6.0 + '@types/react': + specifier: 19.2.2 + version: 19.2.2 + '@types/react-dom': + specifier: 19.2.2 + version: 19.2.2(@types/react@19.2.2) + '@types/react-plotly.js': + specifier: ^2.6.4 + version: 2.6.4 + '@vitejs/plugin-react': + specifier: 5.0.4 + version: 5.0.4(rolldown-vite@7.1.14(@types/node@24.6.0)(esbuild@0.25.10)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2)) + autoprefixer: + specifier: 10.4.21 + version: 10.4.21(postcss@8.5.6) + eslint: + specifier: 9.36.0 + version: 9.36.0(jiti@2.6.1) + eslint-plugin-react-hooks: + specifier: 5.2.0 + version: 5.2.0(eslint@9.36.0(jiti@2.6.1)) + eslint-plugin-react-refresh: + specifier: 0.4.22 + version: 0.4.22(eslint@9.36.0(jiti@2.6.1)) + globals: + specifier: 16.4.0 + version: 16.4.0 + postcss: + specifier: 8.5.6 + version: 8.5.6 + shiki: + specifier: 3.15.0 + version: 3.15.0 + tailwindcss: + specifier: 4.1.17 + version: 4.1.17 + typescript: + specifier: 5.9.3 + version: 5.9.3 + typescript-eslint: + specifier: 8.45.0 + version: 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + vite: + specifier: npm:rolldown-vite@7.1.14 + version: rolldown-vite@7.1.14(@types/node@24.6.0)(esbuild@0.25.10)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2) + docs: dependencies: '@databricks/appkit-ui': @@ -1541,6 +1659,10 @@ packages: '@chevrotain/utils@11.0.3': resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + '@choojs/findup@0.2.1': + resolution: {integrity: sha512-YstAqNb0MCN8PjdLCDfRsBcGVRN41f3vgLvaI0IrIcBp4AqILRSS0DeWNGkicC+f/zRIPJLc+9RURVSepwvfBw==} + hasBin: true + '@clack/core@1.0.1': resolution: {integrity: sha512-WKeyK3NOBwDOzagPR5H08rFk9D/WuN705yEbuZvKqlkmoLM2woKtXb10OO2k1NoSU4SFG947i2/SCYh+2u5e4g==} @@ -2157,15 +2279,9 @@ packages: resolution: {integrity: sha512-lBSBiRruFurFKXr5Hbsl2thmGweAPmddhF3jb99U4EMDA5L+e5Y1rAkOS07Nvrup7HUMBDrCV45meaxZnt28nQ==} engines: {node: '>=20.0'} - '@emnapi/core@1.7.1': - resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} - '@emnapi/core@1.8.1': resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} - '@emnapi/runtime@1.7.1': - resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} - '@emnapi/runtime@1.8.1': resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} @@ -2342,10 +2458,18 @@ packages: resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-helpers@0.3.1': + resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-helpers@0.4.2': resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@0.17.0': resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2354,6 +2478,10 @@ packages: resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@9.36.0': + resolution: {integrity: sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@9.39.1': resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2362,6 +2490,10 @@ packages: resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/plugin-kit@0.4.1': resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2655,6 +2787,48 @@ packages: '@leichtgewicht/ip-codec@2.0.5': resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} + '@mapbox/geojson-rewind@0.5.2': + resolution: {integrity: sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==} + hasBin: true + + '@mapbox/geojson-types@1.0.2': + resolution: {integrity: sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw==} + + '@mapbox/jsonlint-lines-primitives@2.0.2': + resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==} + engines: {node: '>= 0.6'} + + '@mapbox/mapbox-gl-supported@1.5.0': + resolution: {integrity: sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==} + peerDependencies: + mapbox-gl: '>=0.32.1 <2.0.0' + + '@mapbox/point-geometry@0.1.0': + resolution: {integrity: sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==} + + '@mapbox/tiny-sdf@1.2.5': + resolution: {integrity: sha512-cD8A/zJlm6fdJOk6DqPUV8mcpyJkRz2x2R+/fYcWDYG3oWbG7/L7Yl/WqQ1VZCjnL9OTIMAn6c+BC5Eru4sQEw==} + + '@mapbox/tiny-sdf@2.1.0': + resolution: {integrity: sha512-uFJhNh36BR4OCuWIEiWaEix9CA2WzT6CAIcqVjWYpnx8+QDtS+oC4QehRrx5cX4mgWs37MmKnwUejeHxVymzNg==} + + '@mapbox/unitbezier@0.0.0': + resolution: {integrity: sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==} + + '@mapbox/unitbezier@0.0.1': + resolution: {integrity: sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==} + + '@mapbox/vector-tile@1.3.1': + resolution: {integrity: sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==} + + '@mapbox/whoots-js@3.1.0': + resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==} + engines: {node: '>=6.0.0'} + + '@maplibre/maplibre-gl-style-spec@20.4.0': + resolution: {integrity: sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==} + hasBin: true + '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} @@ -2667,9 +2841,6 @@ packages: '@mermaid-js/parser@0.6.3': resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} - '@napi-rs/wasm-runtime@1.0.7': - resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} - '@napi-rs/wasm-runtime@1.1.1': resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} @@ -3385,6 +3556,25 @@ packages: engines: {node: '>=18'} hasBin: true + '@plotly/d3-sankey-circular@0.33.1': + resolution: {integrity: sha512-FgBV1HEvCr3DV7RHhDsPXyryknucxtfnLwPtCKKxdolKyTFYoLX/ibEfX39iFYIL7DYbVeRtP43dbFcrHNE+KQ==} + + '@plotly/d3-sankey@0.7.2': + resolution: {integrity: sha512-2jdVos1N3mMp3QW0k2q1ph7Gd6j5PY1YihBrwpkFnKqO+cqtZq3AdEYUeSGXMeLsBDQYiqTVcihYfk8vr5tqhw==} + + '@plotly/d3@3.8.2': + resolution: {integrity: sha512-wvsNmh1GYjyJfyEBPKJLTMzgf2c2bEbSIL50lmqVUi+o1NHaLPi1Lb4v7VxXXJn043BhNyrxUrWI85Q+zmjOVA==} + + '@plotly/mapbox-gl@1.13.4': + resolution: {integrity: sha512-sR3/Pe5LqT/fhYgp4rT4aSFf1rTsxMbGiH6Hojc7PH36ny5Bn17iVFUjpzycafETURuFbLZUfjODO8LvSI+5zQ==} + engines: {node: '>=6.4.0'} + + '@plotly/point-cluster@3.1.9': + resolution: {integrity: sha512-MwaI6g9scKf68Orpr1pHZ597pYx9uP8UEFXLPbsCmuw3a84obwz6pnMXGc90VhgDNeNiLEdlmuK7CPo+5PIxXw==} + + '@plotly/regl@2.1.2': + resolution: {integrity: sha512-Mdk+vUACbQvjd0m/1JJjOOafmkp/EpmHjISsopEz5Av44CBq7rPC05HHNbYGKVyNUF2zmEoBS/TT0pd0SPFFyw==} + '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -4080,6 +4270,17 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@reduxjs/toolkit@2.11.2': + resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + '@release-it/conventional-changelog@10.0.4': resolution: {integrity: sha512-pU1JkAZBHVk9u0O9CZcaLsqSZHWu0s9WNIFVUq0M9r/WlLpJvrCiSH2OCLo5XyOnWacdMvBjijm+kl6m36SdrA==} engines: {node: ^20.12.0 || >=22.0.0} @@ -4474,15 +4675,33 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@shikijs/core@3.15.0': + resolution: {integrity: sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg==} + + '@shikijs/engine-javascript@3.15.0': + resolution: {integrity: sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg==} + + '@shikijs/engine-oniguruma@3.15.0': + resolution: {integrity: sha512-HnqFsV11skAHvOArMZdLBZZApRSYS4LSztk2K3016Y9VCyZISnlYUYsL2hzlS7tPqKHvNqmI5JSUJZprXloMvA==} + '@shikijs/engine-oniguruma@3.20.0': resolution: {integrity: sha512-Yx3gy7xLzM0ZOjqoxciHjA7dAt5tyzJE3L4uQoM83agahy+PlW244XJSrmJRSBvGYELDhYXPacD4R/cauV5bzQ==} + '@shikijs/langs@3.15.0': + resolution: {integrity: sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A==} + '@shikijs/langs@3.20.0': resolution: {integrity: sha512-le+bssCxcSHrygCWuOrYJHvjus6zhQ2K7q/0mgjiffRbkhM4o1EWu2m+29l0yEsHDbWaWPNnDUTRVVBvBBeKaA==} + '@shikijs/themes@3.15.0': + resolution: {integrity: sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ==} + '@shikijs/themes@3.20.0': resolution: {integrity: sha512-U1NSU7Sl26Q7ErRvJUouArxfM2euWqq1xaSrbqMu2iqa+tSp0D1Yah8216sDYbdDHw4C8b75UpE65eWorm2erQ==} + '@shikijs/types@3.15.0': + resolution: {integrity: sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw==} + '@shikijs/types@3.20.0': resolution: {integrity: sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw==} @@ -4542,6 +4761,9 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@svgr/babel-plugin-add-jsx-attribute@8.0.0': resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} engines: {node: '>=14'} @@ -4627,39 +4849,79 @@ packages: resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} + '@tailwindcss/node@4.1.17': + resolution: {integrity: sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==} + '@tailwindcss/node@4.1.18': resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} + '@tailwindcss/oxide-android-arm64@4.1.17': + resolution: {integrity: sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + '@tailwindcss/oxide-android-arm64@4.1.18': resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} engines: {node: '>= 10'} cpu: [arm64] os: [android] + '@tailwindcss/oxide-darwin-arm64@4.1.17': + resolution: {integrity: sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + '@tailwindcss/oxide-darwin-arm64@4.1.18': resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] + '@tailwindcss/oxide-darwin-x64@4.1.17': + resolution: {integrity: sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + '@tailwindcss/oxide-darwin-x64@4.1.18': resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] + '@tailwindcss/oxide-freebsd-x64@4.1.17': + resolution: {integrity: sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + '@tailwindcss/oxide-freebsd-x64@4.1.18': resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': + resolution: {integrity: sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} engines: {node: '>= 10'} cpu: [arm] os: [linux] + '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': + resolution: {integrity: sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} engines: {node: '>= 10'} @@ -4667,6 +4929,13 @@ packages: os: [linux] libc: [glibc] + '@tailwindcss/oxide-linux-arm64-musl@4.1.17': + resolution: {integrity: sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} @@ -4674,6 +4943,13 @@ packages: os: [linux] libc: [musl] + '@tailwindcss/oxide-linux-x64-gnu@4.1.17': + resolution: {integrity: sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} @@ -4681,6 +4957,13 @@ packages: os: [linux] libc: [glibc] + '@tailwindcss/oxide-linux-x64-musl@4.1.17': + resolution: {integrity: sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + '@tailwindcss/oxide-linux-x64-musl@4.1.18': resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} @@ -4688,6 +4971,18 @@ packages: os: [linux] libc: [musl] + '@tailwindcss/oxide-wasm32-wasi@4.1.17': + resolution: {integrity: sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + '@tailwindcss/oxide-wasm32-wasi@4.1.18': resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} engines: {node: '>=14.0.0'} @@ -4700,25 +4995,69 @@ packages: - '@emnapi/wasi-threads' - tslib + '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': + resolution: {integrity: sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] + '@tailwindcss/oxide-win32-x64-msvc@4.1.17': + resolution: {integrity: sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} engines: {node: '>= 10'} cpu: [x64] os: [win32] + '@tailwindcss/oxide@4.1.17': + resolution: {integrity: sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==} + engines: {node: '>= 10'} + '@tailwindcss/oxide@4.1.18': resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} engines: {node: '>= 10'} + '@tailwindcss/postcss@4.1.17': + resolution: {integrity: sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==} + '@tailwindcss/postcss@4.1.18': resolution: {integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==} + '@tanstack/history@1.133.19': + resolution: {integrity: sha512-Y866qBVVprdQkmO0/W1AFBI8tiQy398vFeIwP+VrRWCOzs3VecxSVzAvaOM4iHfkJz81fFAZMhLLjDVoPikD+w==} + engines: {node: '>=12'} + + '@tanstack/react-router-devtools@1.133.22': + resolution: {integrity: sha512-YG498dyttY7yszEGo0iE4S3ymNrX+PSWXbP7zy94RhLf3mizupInxlKaypxhIU16toKiyOQzgFgOqi6v4RqfEQ==} + engines: {node: '>=12'} + peerDependencies: + '@tanstack/react-router': ^1.133.22 + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-router@1.133.22': + resolution: {integrity: sha512-0tg2yoXVMvvgR3UdOhEX9ICmgZ/Ou/I8VOl07exSYEJYfyCr5nhtB/62F9NGbuUZVrJnCzc8Rz0e4/MYU18pIg==} + engines: {node: '>=12'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-store@0.7.7': + resolution: {integrity: sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/react-table@8.21.3': resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} engines: {node: '>=12'} @@ -4726,10 +5065,67 @@ packages: react: '>=16.8' react-dom: '>=16.8' + '@tanstack/router-cli@1.133.20': + resolution: {integrity: sha512-XFghXTGUDzBhLbe5UWikLDbcAcuDfqWtlJvyVhDl7rYV7Pvkdb8hGgbxsriUpaVKPx5nmud8JGINIW56lQUTyA==} + engines: {node: '>=12'} + hasBin: true + + '@tanstack/router-core@1.133.20': + resolution: {integrity: sha512-cO8E6XA0vMX2BaPZck9kfgXK76e6Lqo13GmXEYxtXshmW8cIlgcLHhBDKnI/sCjIy9OPY2sV1qrGHtcxJy/4ew==} + engines: {node: '>=12'} + + '@tanstack/router-devtools-core@1.133.22': + resolution: {integrity: sha512-Pcpyrd3rlNA6C1jnL6jy4pC/8s4PN7270RM7+krnlKex1Rk3REgQ5LXAaAJJxOXS2coY14tiQtfQS3gx+H3b4w==} + engines: {node: '>=12'} + peerDependencies: + '@tanstack/router-core': ^1.133.20 + csstype: ^3.0.10 + solid-js: '>=1.9.5' + tiny-invariant: ^1.3.3 + peerDependenciesMeta: + csstype: + optional: true + + '@tanstack/router-generator@1.133.20': + resolution: {integrity: sha512-63lhmNNoVfqTgnSx5MUnEl/QBKSN6hA1sWLhZSQhCjLp9lrWbCXM8l9QpG3Tgzq/LdX7jjDMf783sUL4p4NbYw==} + engines: {node: '>=12'} + + '@tanstack/router-plugin@1.133.22': + resolution: {integrity: sha512-VVUazrxqFyon9bFSFY2mysgTbQAH5BV8kP8Gq1IHd7AxlboRW9tnj6TQcy8KGgG/KPCbKB9CFZtvSheKqrAVQg==} + engines: {node: '>=12'} + peerDependencies: + '@rsbuild/core': '>=1.0.2' + '@tanstack/react-router': ^1.133.22 + vite: '>=5.0.0 || >=6.0.0 || >=7.0.0' + vite-plugin-solid: ^2.11.10 + webpack: '>=5.92.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@tanstack/react-router': + optional: true + vite: + optional: true + vite-plugin-solid: + optional: true + webpack: + optional: true + + '@tanstack/router-utils@1.133.19': + resolution: {integrity: sha512-WEp5D2gPxvlLDRXwD/fV7RXjYtqaqJNXKB/L6OyZEbT+9BG/Ib2d7oG9GSUZNNMGPGYAlhBUOi3xutySsk6rxA==} + engines: {node: '>=12'} + + '@tanstack/store@0.7.7': + resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==} + '@tanstack/table-core@8.21.3': resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} + '@tanstack/virtual-file-routes@1.133.19': + resolution: {integrity: sha512-IKwZENsK7owmW1Lm5FhuHegY/SyQ8KqtL/7mTSnzoKJgfzhrrf9qwKB1rmkKkt+svUuy/Zw3uVEpZtUzQruWtA==} + engines: {node: '>=12'} + '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} @@ -4756,6 +5152,21 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} + '@turf/area@7.3.5': + resolution: {integrity: sha512-sSn80wPT7XfBIDN3vurCPxhk9W4U8ozS/XImSqeLN8qveTICOxzZkhsGDMp0CuncaN+plWut4a2TdNM7mzZB6Q==} + + '@turf/bbox@7.3.5': + resolution: {integrity: sha512-oG1ya/HtBjAIg4TimbWx+nOYPbY0bCvt82Bq8tm6sBw3qqtbOyRSfDz79Sq90TnH7DXJprJ1qnVGKNtZ6jemfw==} + + '@turf/centroid@7.3.5': + resolution: {integrity: sha512-hkWaqwGFdOn6Tf0EWfn2yn1XZ1FWE1h2C5ZWstDMu/FxYO5DB+YjlmOFPl4K6SmSOEgdV07eK2vDCyPeTHqKGA==} + + '@turf/helpers@7.3.5': + resolution: {integrity: sha512-E/NMGV5MwbjjP7AJXBtsanC3yY8N2MQ87IGdIgkB2ji5AtBpwnH4L3gEqpYN4RlCJJWbLbzO91BbKv2waUd0eg==} + + '@turf/meta@7.3.5': + resolution: {integrity: sha512-r+ohqxoyqeigFB0oFrQx/YEHIkOKqcKpCjvZkvZs7Tkv+IFco5MezAd2zd4rzK+0DfFgDP3KpJc7HqrYjvEjhg==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -4924,6 +5335,9 @@ packages: '@types/express@4.17.25': resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} + '@types/geojson-vt@3.2.5': + resolution: {integrity: sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==} + '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} @@ -4969,6 +5383,12 @@ packages: '@types/lodash@4.17.24': resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + '@types/mapbox__point-geometry@0.1.4': + resolution: {integrity: sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==} + + '@types/mapbox__vector-tile@1.3.4': + resolution: {integrity: sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -5002,6 +5422,9 @@ packages: '@types/node@24.10.1': resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + '@types/node@24.6.0': + resolution: {integrity: sha512-F1CBxgqwOMc4GKJ7eY22hWhBVQuMYTtqI8L0FcszYcpYX0fzfDGpez22Xau8Mgm7O9fI+zA/TYIdq3tGWfweBA==} + '@types/node@24.7.2': resolution: {integrity: sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==} @@ -5021,6 +5444,9 @@ packages: '@types/parse5@5.0.3': resolution: {integrity: sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==} + '@types/pbf@3.0.5': + resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==} + '@types/pg-pool@2.0.6': resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} @@ -5030,6 +5456,9 @@ packages: '@types/pg@8.16.0': resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==} + '@types/plotly.js@3.0.10': + resolution: {integrity: sha512-q+MgO4aajC2HrO7FllTYWzrpdfbTjboSMfjkz/aXKjg1v7HNo1zMEFfAW7quKfk6SL+bH74A5ThBEps/7hZxOA==} + '@types/prismjs@1.26.5': resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} @@ -5049,6 +5478,9 @@ packages: peerDependencies: '@types/react': ^19.2.0 + '@types/react-plotly.js@2.6.4': + resolution: {integrity: sha512-AU6w1u3qEGM0NmBA69PaOgNc0KPFA/+qkH6Uu9EBTJ45/WYOUoXi9AF5O15PRM2klpHSiHAAs4WnlI+OZAFmUA==} + '@types/react-router-config@5.0.11': resolution: {integrity: sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw==} @@ -5088,6 +5520,9 @@ packages: '@types/sockjs@0.3.36': resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==} + '@types/supercluster@7.1.3': + resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==} + '@types/tedious@4.0.14': resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} @@ -5100,6 +5535,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/validator@13.15.10': resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} @@ -5112,6 +5550,14 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + '@typescript-eslint/eslint-plugin@8.45.0': + resolution: {integrity: sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.45.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/eslint-plugin@8.49.0': resolution: {integrity: sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5120,6 +5566,13 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/parser@8.45.0': + resolution: {integrity: sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/parser@8.49.0': resolution: {integrity: sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5127,22 +5580,45 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/project-service@8.45.0': + resolution: {integrity: sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/project-service@8.49.0': resolution: {integrity: sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/scope-manager@8.45.0': + resolution: {integrity: sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.49.0': resolution: {integrity: sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.45.0': + resolution: {integrity: sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/tsconfig-utils@8.49.0': resolution: {integrity: sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/type-utils@8.45.0': + resolution: {integrity: sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/type-utils@8.49.0': resolution: {integrity: sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5150,16 +5626,33 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/types@8.45.0': + resolution: {integrity: sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.49.0': resolution: {integrity: sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.45.0': + resolution: {integrity: sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/typescript-estree@8.49.0': resolution: {integrity: sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/utils@8.45.0': + resolution: {integrity: sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/utils@8.49.0': resolution: {integrity: sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5167,6 +5660,10 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/visitor-keys@8.45.0': + resolution: {integrity: sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.49.0': resolution: {integrity: sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5286,6 +5783,9 @@ packages: resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==} engines: {node: ^20.17.0 || >=22.9.0} + abs-svg-path@0.1.1: + resolution: {integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -5310,6 +5810,11 @@ packages: resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} engines: {node: '>=0.4.0'} + acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -5452,12 +5957,25 @@ packages: resolution: {integrity: sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==} engines: {node: '>=12.17'} + array-bounds@1.0.1: + resolution: {integrity: sha512-8wdW3ZGk6UjMPJx/glyEt0sLzzwAE1bhToPsO1W2pbpR2gULyxe3BjSiuJFheP50T/GgODVPz2fuMUmIywt8cQ==} + + array-find-index@1.0.2: + resolution: {integrity: sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==} + engines: {node: '>=0.10.0'} + array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} array-ify@1.0.0: resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + array-normalize@1.1.4: + resolution: {integrity: sha512-fCp0wKFLjvSPmCn4F5Tiw4M3lpMZoHlCjfcs7nNzuj3vqQQ1/a8cgB9DXcpDSn18c+coLnaW7rqfcYCvKbyJXg==} + + array-range@1.0.1: + resolution: {integrity: sha512-shdaI1zT3CVNL2hnx9c0JMc0ZogGaxDs5e85akgHWKYa0yVbIyp06Ind3dVkTj/uuFrzaHBOyqFzo+VV6aXgtA==} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -5474,6 +5992,10 @@ packages: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} @@ -5488,6 +6010,13 @@ packages: autocomplete.js@0.37.1: resolution: {integrity: sha512-PgSe9fHYhZEsm/9jggbjtVsGXJkPLvd+9mC7gZJ662vVL5CRWEtm/mIrrzCx0MrNxHVwxD5d00UOn6NsmL2LUQ==} + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + autoprefixer@10.4.23: resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==} engines: {node: ^10 || ^12 || >=14} @@ -5499,6 +6028,9 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + babel-dead-code-elimination@1.0.12: + resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} + babel-loader@9.2.1: resolution: {integrity: sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==} engines: {node: '>= 14.15.0'} @@ -5537,13 +6069,13 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.8.32: - resolution: {integrity: sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==} - hasBin: true - baseline-browser-mapping@2.9.7: resolution: {integrity: sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==} hasBin: true @@ -5582,9 +6114,21 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + binary-search-bounds@2.0.5: + resolution: {integrity: sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==} + birpc@4.0.0: resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} + bit-twiddle@1.0.2: + resolution: {integrity: sha512-B9UhK0DKFZhoTFcfvAzhqsjStvGJp9vYWf3+6SNTtdSQnvIgfkHbgHrg/e4+TH71N2GDu8tpmCVoyfrL1d7ntA==} + + bitmap-sdf@1.0.4: + resolution: {integrity: sha512-1G3U4n5JE6RAiALMxu0p1XmeZkTeCwGKykzsLTCqVzfSDaN6S7fKnkIkfejogz+iwqBWc0UYAIKnKHNN7pSfDg==} + + bl@2.2.1: + resolution: {integrity: sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -5624,11 +6168,6 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.0: - resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -5724,12 +6263,12 @@ packages: caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} - caniuse-lite@1.0.30001757: - resolution: {integrity: sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==} - caniuse-lite@1.0.30001760: resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==} + canvas-fit@1.5.0: + resolution: {integrity: sha512-onIcjRpz69/Hx5bB5HGbYKUF2uC6QT6Gp+pfpGm3A7mPfcluSLV5v4Zu+oflDUwLdUw0rLIBhUbi0v8hM4FJQQ==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -5824,6 +6363,9 @@ packages: cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + clamp@1.0.1: + resolution: {integrity: sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA==} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -5888,13 +6430,37 @@ packages: collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} + color-alpha@1.0.4: + resolution: {integrity: sha512-lr8/t5NPozTSqli+duAN+x+no/2WaKTeWvxhHGN+aXT6AJ8vPlzLa7UriyjWak0pSC2jHol9JgjBYnnHsGha9A==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} + color-id@1.1.0: + resolution: {integrity: sha512-2iRtAn6dC/6/G7bBIo0uupVrIne1NsQJvJxZOBCzQOfk7jRq97feaDZ3RdzuHakRXXnHGNwglto3pqtRx1sX0g==} + color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-normalize@1.5.0: + resolution: {integrity: sha512-rUT/HDXMr6RFffrR53oX3HGWkDOP9goSAQGBkUaAYKjOE2JxozccdGyufageWDlInRAjm/jYPrf/Y38oa+7obw==} + + color-parse@1.4.3: + resolution: {integrity: sha512-BADfVl/FHkQkyo8sRBwMYBqemqsgnu7JZAwUgvBvuwwuNUZAhSvLTbsEErS5bQXzOjDR0dWzJ4vXN2Q+QoPx0A==} + + color-parse@2.0.0: + resolution: {integrity: sha512-g2Z+QnWsdHLppAbrpcFWo629kLOnOPtpxYV69GCqm92gqSgyXbzlfyN3MXs0412fPBkFmiuS+rXposgBgBa6Kg==} + + color-rgba@2.4.0: + resolution: {integrity: sha512-Nti4qbzr/z2LbUWySr7H9dk3Rl7gZt7ihHAxlgT4Ho90EXWkjtkL1avTleu9yeGuqrt/chxTB6GKK8nZZ6V0+Q==} + + color-rgba@3.0.0: + resolution: {integrity: sha512-PPwZYkEY3M2THEHHV6Y95sGUie77S7X8v+h1r6LSAPF3/LL2xJ8duUXSrkic31Nzc4odPwHgUbiX/XuTYzQHQg==} + + color-space@2.3.2: + resolution: {integrity: sha512-BcKnbOEsOarCwyoLstcoEztwT0IJxqqQkNwDuA3a65sICvvHL2yoeV13psoDFh5IuiOMnIOKdQDwB4Mk3BypiA==} + color-support@1.1.3: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true @@ -5975,6 +6541,10 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + concat-stream@1.6.2: + resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} + engines: {'0': node >= 0.8} + concat-stream@2.0.0: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} @@ -6067,6 +6637,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-es@2.0.1: + resolution: {integrity: sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA==} + cookie-signature@1.0.7: resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} @@ -6124,6 +6697,9 @@ packages: typescript: optional: true + country-regex@1.1.0: + resolution: {integrity: sha512-iSPlClZP8vX7MC3/u6s3lrDuoQyhQukh5LyABJ3hvfzbQ3Yyayd4fp04zjLnfi267B/B2FkumcWWgrbban7sSA==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -6144,6 +6720,24 @@ packages: peerDependencies: postcss: ^8.0.9 + css-font-size-keywords@1.0.0: + resolution: {integrity: sha512-Q+svMDbMlelgCfH/RVDKtTDaf5021O486ZThQPIpahnIjUkMUslC+WuOQSWTgGSrNCH08Y7tYNEmmy0hkfMI8Q==} + + css-font-stretch-keywords@1.0.1: + resolution: {integrity: sha512-KmugPO2BNqoyp9zmBIUGwt58UQSfyk1X5DbOlkb2pckDXFSAfjsD5wenb88fNrD6fvS+vu90a/tsPpb9vb0SLg==} + + css-font-style-keywords@1.0.1: + resolution: {integrity: sha512-0Fn0aTpcDktnR1RzaBYorIxQily85M2KXRpzmxQPgh8pxUN9Fcn00I8u9I3grNr1QXVgCl9T5Imx0ZwKU973Vg==} + + css-font-weight-keywords@1.0.0: + resolution: {integrity: sha512-5So8/NH+oDD+EzsnF4iaG4ZFHQ3vaViePkL1ZbZ5iC/KrsCY+WHq/lvOgrtmuOQ9pBBZ1ADGpaf+A4lj1Z9eYA==} + + css-font@1.2.0: + resolution: {integrity: sha512-V4U4Wps4dPDACJ4WpgofJ2RT5Yqwe1lEH6wlOOaIxMi0gTjdIijsc5FmxQlZ7ZZyKQkkutqqvULOp07l9c7ssA==} + + css-global-keywords@1.0.1: + resolution: {integrity: sha512-X1xgQhkZ9n94WDwntqst5D/FKkmiU0GlJSFZSV3kLvyJ1WC5VeyoXDOuleUD+SIuH9C7W05is++0Woh0CGfKjQ==} + css-has-pseudo@7.0.3: resolution: {integrity: sha512-oG+vKuGyqe/xvEMoxAQrhi7uY16deJR3i7wwhBerVrGQKSqUC5GiOVxTpM9F9B9hw0J+eKeOWLH7E9gZ1Dr5rA==} engines: {node: '>=18'} @@ -6205,6 +6799,9 @@ packages: css-selector-parser@3.3.0: resolution: {integrity: sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==} + css-system-font-keywords@1.0.0: + resolution: {integrity: sha512-1umTtVd/fXS25ftfjB71eASCrYhilmEsvDEI6wG/QplnmlfmVM5HkZ/ZX46DT5K3eblFPgLUHt5BRCb0YXkSFA==} + css-tree@2.2.1: resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} @@ -6221,6 +6818,9 @@ packages: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} + csscolorparser@1.0.3: + resolution: {integrity: sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==} + cssdb@8.5.2: resolution: {integrity: sha512-Pmoj9RmD8RIoIzA2EQWO4D4RMeDts0tgAH0VXdlNdxjuBGI3a9wMOIcUwaPNmD4r2qtIa06gqkIf7sECl+cBCg==} @@ -6278,6 +6878,9 @@ packages: resolution: {integrity: sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==} engines: {node: '>=0.10'} + d3-array@1.2.4: + resolution: {integrity: sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==} + d3-array@2.12.1: resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} @@ -6297,6 +6900,9 @@ packages: resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} engines: {node: '>=12'} + d3-collection@1.0.7: + resolution: {integrity: sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==} + d3-color@3.1.0: resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} engines: {node: '>=12'} @@ -6309,6 +6915,9 @@ packages: resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} engines: {node: '>=12'} + d3-dispatch@1.0.6: + resolution: {integrity: sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==} + d3-dispatch@3.0.1: resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} engines: {node: '>=12'} @@ -6330,18 +6939,34 @@ packages: resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} engines: {node: '>=12'} + d3-force@1.2.1: + resolution: {integrity: sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==} + d3-force@3.0.0: resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} engines: {node: '>=12'} + d3-format@1.4.5: + resolution: {integrity: sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==} + d3-format@3.1.0: resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} engines: {node: '>=12'} + d3-geo-projection@2.9.0: + resolution: {integrity: sha512-ZULvK/zBn87of5rWAfFMc9mJOipeSo57O+BBitsKIXmU4rTVAnX1kSsJkE0R+TxY8pGNoM1nbyRRE7GYHhdOEQ==} + hasBin: true + + d3-geo@1.12.1: + resolution: {integrity: sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==} + d3-geo@3.1.1: resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} engines: {node: '>=12'} + d3-hierarchy@1.1.9: + resolution: {integrity: sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==} + d3-hierarchy@3.1.2: resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} engines: {node: '>=12'} @@ -6361,6 +6986,9 @@ packages: resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} engines: {node: '>=12'} + d3-quadtree@1.0.7: + resolution: {integrity: sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==} + d3-quadtree@3.0.1: resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} engines: {node: '>=12'} @@ -6391,14 +7019,23 @@ packages: resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} engines: {node: '>=12'} + d3-time-format@2.3.0: + resolution: {integrity: sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==} + d3-time-format@4.1.0: resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} engines: {node: '>=12'} + d3-time@1.1.0: + resolution: {integrity: sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==} + d3-time@3.1.0: resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} engines: {node: '>=12'} + d3-timer@1.0.10: + resolution: {integrity: sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==} + d3-timer@3.0.1: resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} engines: {node: '>=12'} @@ -6417,6 +7054,10 @@ packages: resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} engines: {node: '>=12'} + d@1.0.2: + resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} + engines: {node: '>=0.12'} + dagre-d3-es@7.0.13: resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==} @@ -6456,6 +7097,14 @@ packages: supports-color: optional: true + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -6533,6 +7182,9 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + defined@1.0.1: + resolution: {integrity: sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==} + defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} @@ -6562,6 +7214,9 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-kerning@2.1.2: + resolution: {integrity: sha512-I3JIbrnKPAntNLl1I6TpSQQdQ4AutYzv/sKMFKbepawV/hlH0GmYKhUoOEMd4xqaUHT+Bm0f4127lh5qs1m1tw==} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -6580,6 +7235,10 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -6667,6 +7326,9 @@ packages: resolution: {integrity: sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + draw-svg-path@1.0.0: + resolution: {integrity: sha512-P8j3IHxcgRMcY6sDzr0QvJDLzBnJJqpTG33UZ2Pvp8rw0apCHhJCWqYprqrXjrgHnJ6tuhP1iTJSAodPDHxwkg==} + drizzle-orm@0.45.1: resolution: {integrity: sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA==} peerDependencies: @@ -6768,13 +7430,29 @@ packages: oxc-resolver: optional: true + dtype@2.0.0: + resolution: {integrity: sha512-s2YVcLKdFGS0hpFqJaTwscsyt0E8nNFdmo73Ocd81xNPj4URI4rj6D60A+vFMIw7BXWlb4yRkEwfBqcZzPGiZg==} + engines: {node: '>= 0.8.0'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + dup@1.0.0: + resolution: {integrity: sha512-Bz5jxMMC0wgp23Zm15ip1x8IhYRqJvF3nFC0UInJUDkN1z4uNPk9jTnfCUJXbOGiQ1JbXLQsiV41Fb+HXcj5BA==} + duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + duplexify@3.7.1: + resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} + + earcut@2.2.4: + resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==} + + earcut@3.0.2: + resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -6797,12 +7475,15 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.262: - resolution: {integrity: sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==} - electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + element-size@1.1.1: + resolution: {integrity: sha512-eaN+GMOq/Q+BIWy0ybsgpcYImjGIdNLyjLFJU4XsLHXYQao5jCNb36GyN6C2qwmDDYSfIBmKpPpr4VnBdLCsPQ==} + + elementary-circuits-directed-graph@1.3.1: + resolution: {integrity: sha512-ZEiB5qkn2adYmpXGnJKkxT8uJHlW/mxmBpmeqawEHzPxh9HkLD4/1mFYX5l0On+f6rcPIt8/EWlRU2Vo3fX6dQ==} + embla-carousel-react@8.6.0: resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==} peerDependencies: @@ -6898,6 +7579,23 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-toolkit@1.46.0: + resolution: {integrity: sha512-IToJ6ct9OLl5zz6WsC/1vZEwfSZ7Myil+ygl5Tf30Xjn9AEkzNB4kqp2G7VUJKF1DtTx/ra5M5KLlXvzOg51BA==} + + es5-ext@0.10.64: + resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} + engines: {node: '>=0.10'} + + es6-iterator@2.0.3: + resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} + + es6-symbol@3.1.4: + resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} + engines: {node: '>=0.12'} + + es6-weak-map@2.0.3: + resolution: {integrity: sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==} + esast-util-from-estree@2.0.0: resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} @@ -6937,12 +7635,23 @@ packages: engines: {node: '>=6.0'} hasBin: true + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + eslint-plugin-react-hooks@7.0.1: resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} engines: {node: '>=18'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + eslint-plugin-react-refresh@0.4.22: + resolution: {integrity: sha512-atkAG6QaJMGoTLc4MDAP+rqZcfwQuTIh2IqHWFLy2TEjxr0MOK+5BSG4RzL2564AAPpZkDRsZXAUz68kjnU6Ug==} + peerDependencies: + eslint: '>=8.40' + eslint-plugin-react-refresh@0.4.24: resolution: {integrity: sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==} peerDependencies: @@ -6964,6 +7673,16 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint@9.36.0: + resolution: {integrity: sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + eslint@9.39.1: resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -6974,6 +7693,10 @@ packages: jiti: optional: true + esniff@2.0.1: + resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} + engines: {node: '>=0.10'} + espree@10.4.0: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -7043,6 +7766,9 @@ packages: resolution: {integrity: sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==} engines: {node: '>= 0.8'} + event-emitter@0.3.5: + resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} @@ -7083,6 +7809,9 @@ packages: exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + ext@1.7.0: + resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} @@ -7090,6 +7819,10 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + falafel@2.2.5: + resolution: {integrity: sha512-HuC1qF9iTnHDnML9YZAdCDQwT0yKl/U55K4XSUXqGAA2GLoafFgWRqdAbhWJxXaYD4pyoVxAJ8wH670jMpI9DQ==} + engines: {node: '>=0.4.0'} + fast-content-type-parse@3.0.0: resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} @@ -7104,6 +7837,9 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-isnumeric@1.1.4: + resolution: {integrity: sha512-1mM8qOr2LYz8zGaUdmiqRDiuue00Dxjgcb1NQR7TnhLVh6sQyngP9xvLo7Sl7LZpP/sk5eb+bcyWXw530NTBZw==} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -7208,6 +7944,9 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + flatten-vertex-data@1.0.2: + resolution: {integrity: sha512-BvCBFK2NZqerFTdMDgqfHBwxYWnxeCkwONsw6PvBMcUXqo8U/KDWwmXhqx1x2kLIg7DqIsJfOaJFOmlua3Lxuw==} + follow-redirects@1.15.11: resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} @@ -7217,6 +7956,12 @@ packages: debug: optional: true + font-atlas@2.1.0: + resolution: {integrity: sha512-kP3AmvX+HJpW4w3d+PiPR2X6E1yvsBXt2yhuCw+yReO9F1WYhvZwx3c95DGZGwg9xYzDGrgJYa885xmVA+28Cg==} + + font-measure@1.2.2: + resolution: {integrity: sha512-mRLEpdrWzKe9hbfaF3Qpr06TAjquuBVP5cHy4b3hyeNdjc9i0PO6HniGsX5vjL5OWv7+Bd++NiooNpT/s8BvIA==} + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -7253,6 +7998,9 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} @@ -7260,6 +8008,9 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + from2@2.3.0: + resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -7309,10 +8060,19 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + geojson-vt@3.2.1: + resolution: {integrity: sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==} + + geojson-vt@4.0.2: + resolution: {integrity: sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-canvas-context@1.0.2: + resolution: {integrity: sha512-LnpfLf/TNzr9zVOGiIY6aKCz8EKuXmlYNV7CM2pUjBa/B+c2I15tS7KLySep75+FuerJdmArvJLcsAXWEy2H0A==} + get-east-asian-width@1.4.0: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} @@ -7373,6 +8133,18 @@ packages: github-slugger@1.5.0: resolution: {integrity: sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==} + gl-mat4@1.2.0: + resolution: {integrity: sha512-sT5C0pwB1/e9G9AvAoLsoaJtbMGjfd/jfxo8jMCKqYYEnjZuFvqV5rehqar0538EmssjdDeiEWnKyBSTw7quoA==} + + gl-matrix@3.4.4: + resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==} + + gl-text@1.4.0: + resolution: {integrity: sha512-o47+XBqLCj1efmuNyCHt7/UEJmB9l66ql7pnobD6p+sgmBUdzfMZXIF0zD2+KRfpd99DJN+QXdvTFAGCKCVSmQ==} + + gl-util@3.1.3: + resolution: {integrity: sha512-dvRTggw5MSkJnCbh74jZzSoTOGnVYK+Bt+Ckqm39CVcl6+zSsxqWk4lr5NKhkqXHL6qvZAU9h17ZF8mIskY9mA==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -7412,10 +8184,18 @@ packages: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} engines: {node: '>=10'} + global-prefix@4.0.0: + resolution: {integrity: sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==} + engines: {node: '>=16'} + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + globals@16.4.0: + resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + engines: {node: '>=18'} + globals@16.5.0: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} @@ -7431,6 +8211,57 @@ packages: globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + glsl-inject-defines@1.0.3: + resolution: {integrity: sha512-W49jIhuDtF6w+7wCMcClk27a2hq8znvHtlGnrYkSWEr8tHe9eA2dcnohlcAmxLYBSpSSdzOkRdyPTrx9fw49+A==} + + glsl-resolve@0.0.1: + resolution: {integrity: sha512-xxFNsfnhZTK9NBhzJjSBGX6IOqYpvBHxxmo+4vapiljyGNCY0Bekzn0firQkQrazK59c1hYxMDxYS8MDlhw4gA==} + + glsl-token-assignments@2.0.2: + resolution: {integrity: sha512-OwXrxixCyHzzA0U2g4btSNAyB2Dx8XrztY5aVUCjRSh4/D0WoJn8Qdps7Xub3sz6zE73W3szLrmWtQ7QMpeHEQ==} + + glsl-token-defines@1.0.0: + resolution: {integrity: sha512-Vb5QMVeLjmOwvvOJuPNg3vnRlffscq2/qvIuTpMzuO/7s5kT+63iL6Dfo2FYLWbzuiycWpbC0/KV0biqFwHxaQ==} + + glsl-token-depth@1.1.2: + resolution: {integrity: sha512-eQnIBLc7vFf8axF9aoi/xW37LSWd2hCQr/3sZui8aBJnksq9C7zMeUYHVJWMhFzXrBU7fgIqni4EhXVW4/krpg==} + + glsl-token-descope@1.0.2: + resolution: {integrity: sha512-kS2PTWkvi/YOeicVjXGgX5j7+8N7e56srNDEHDTVZ1dcESmbmpmgrnpjPcjxJjMxh56mSXYoFdZqb90gXkGjQw==} + + glsl-token-inject-block@1.1.0: + resolution: {integrity: sha512-q/m+ukdUBuHCOtLhSr0uFb/qYQr4/oKrPSdIK2C4TD+qLaJvqM9wfXIF/OOBjuSA3pUoYHurVRNao6LTVVUPWA==} + + glsl-token-properties@1.0.1: + resolution: {integrity: sha512-dSeW1cOIzbuUoYH0y+nxzwK9S9O3wsjttkq5ij9ZGw0OS41BirKJzzH48VLm8qLg+au6b0sINxGC0IrGwtQUcA==} + + glsl-token-scope@1.1.2: + resolution: {integrity: sha512-YKyOMk1B/tz9BwYUdfDoHvMIYTGtVv2vbDSLh94PT4+f87z21FVdou1KNKgF+nECBTo0fJ20dpm0B1vZB1Q03A==} + + glsl-token-string@1.0.1: + resolution: {integrity: sha512-1mtQ47Uxd47wrovl+T6RshKGkRRCYWhnELmkEcUAPALWGTFe2XZpH3r45XAwL2B6v+l0KNsCnoaZCSnhzKEksg==} + + glsl-token-whitespace-trim@1.0.0: + resolution: {integrity: sha512-ZJtsPut/aDaUdLUNtmBYhaCmhIjpKNg7IgZSfX5wFReMc2vnj8zok+gB/3Quqs0TsBSX/fGnqUUYZDqyuc2xLQ==} + + glsl-tokenizer@2.1.5: + resolution: {integrity: sha512-XSZEJ/i4dmz3Pmbnpsy3cKh7cotvFlBiZnDOwnj/05EwNp2XrhQ4XKJxT7/pDt4kp4YcpRSKz8eTV7S+mwV6MA==} + + glslify-bundle@5.1.1: + resolution: {integrity: sha512-plaAOQPv62M1r3OsWf2UbjN0hUYAB7Aph5bfH58VxJZJhloRNbxOL9tl/7H71K7OLJoSJ2ZqWOKk3ttQ6wy24A==} + + glslify-deps@1.3.2: + resolution: {integrity: sha512-7S7IkHWygJRjcawveXQjRXLO2FTjijPDYC7QfZyAQanY+yGLCFHYnPtsGT9bdyHiwPTw/5a1m1M9hamT2aBpag==} + + glslify@7.1.1: + resolution: {integrity: sha512-bud98CJ6kGZcP9Yxcsi7Iz647wuDz3oN+IZsjCRi5X1PI7t/xPKeL0mOwXJjo+CRZMqvq0CkSJiywCcY7kVYog==} + hasBin: true + + goober@2.1.18: + resolution: {integrity: sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==} + peerDependencies: + csstype: ^3.0.10 + google-auth-library@10.5.0: resolution: {integrity: sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==} engines: {node: '>=18'} @@ -7461,10 +8292,16 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + gray-matter@4.0.3: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} + grid-index@1.1.0: + resolution: {integrity: sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==} + gtoken@8.0.0: resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==} engines: {node: '>=18'} @@ -7488,6 +8325,12 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-hover@1.0.1: + resolution: {integrity: sha512-0G6w7LnlcpyDzpeGUTuT0CEw05+QlMuGVk1IHNAlHrGJITGodjZu3x8BNDUMfKJSZXNB2ZAclqc1bvrd+uUpfg==} + + has-passive-events@1.0.0: + resolution: {integrity: sha512-2vSj6IeIsgvsRMyeQ0JaCX5Q3lX4zMn5HpoVc7MEhQ6pv8Iq9rsXjsp+E5ZwaT7T0xhMT0KmU8gtt1EFVdbJiw==} + has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} @@ -7786,6 +8629,12 @@ packages: immediate@3.3.0: resolution: {integrity: sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==} + immer@10.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} + + immer@11.1.4: + resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -7837,6 +8686,10 @@ packages: resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ini@4.1.3: + resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ini@6.0.0: resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} engines: {node: ^20.17.0 || >=22.9.0} @@ -7894,6 +8747,9 @@ packages: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} + is-browser@2.1.0: + resolution: {integrity: sha512-F5rTJxDQ2sW81fcfOR1GnCXT6sVJC104fCyfj+mjpwNEwaPYSn5fte5jiHmBg3DHsIoL/l8Kvw5VN5SsTRcRFQ==} + is-buffer@2.0.5: resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} engines: {node: '>=4'} @@ -7931,6 +8787,14 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-finite@1.1.0: + resolution: {integrity: sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==} + engines: {node: '>=0.10.0'} + + is-firefox@1.0.3: + resolution: {integrity: sha512-6Q9ITjvWIm0Xdqv+5U12wgOKEM2KoBw4Y926m0OFkvlCxnbG94HKAsVz8w3fWcfAS5YA2fJORXX1dLrkprCCxA==} + engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -7963,6 +8827,9 @@ packages: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} engines: {node: '>=12'} + is-mobile@4.0.0: + resolution: {integrity: sha512-mlcHZA84t1qLSuWkt2v0I2l61PYdyQDt4aG1mLIXF5FDMm4+haBCxCPYSr/uwqQNRk1MiTizn0ypEuRAOLRAew==} + is-network-error@1.3.0: resolution: {integrity: sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==} engines: {node: '>=16'} @@ -7987,6 +8854,10 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + is-plain-obj@2.1.0: resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} engines: {node: '>=8'} @@ -8025,6 +8896,12 @@ packages: resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} engines: {node: '>=18'} + is-string-blank@1.0.1: + resolution: {integrity: sha512-9H+ZBCVs3L9OYqv8nuUAzpcT9OTgMD1yAWrG7ihlnibdkbtB850heAmYWxHuXc4CHy4lKeK69tN+ny1K7gBIrw==} + + is-svg-path@1.0.2: + resolution: {integrity: sha512-Lj4vePmqpPR1ZnRctHv8ltSh1OrSxHkhUkd7wi+VQdcdP15/KvQFyk7LhNuM7ZW0EVbJz8kZLVmL9quLrfq4Kg==} + is-text-path@2.0.0: resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} engines: {node: '>=8'} @@ -8061,9 +8938,17 @@ packages: isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isbot@5.1.39: + resolution: {integrity: sha512-obH0yYahGXdzNxo+djmHhBYThUKDkz565cxkIlt2L9hXfv1NlaLKoDBHo6KxXsYrIXx2RK3x5vY36CfZcobxEw==} + engines: {node: '>=18'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isexe@3.1.5: + resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} + engines: {node: '>=18'} + isexe@4.0.0: resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} engines: {node: '>=20'} @@ -8187,6 +9072,9 @@ packages: json-stringify-nice@1.1.4: resolution: {integrity: sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw==} + json-stringify-pretty-compact@4.0.0: + resolution: {integrity: sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -8222,6 +9110,12 @@ packages: resolution: {integrity: sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==} hasBin: true + kdbush@3.0.0: + resolution: {integrity: sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==} + + kdbush@4.0.2: + resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -8480,6 +9374,11 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} + lucide-react@0.546.0: + resolution: {integrity: sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lucide-react@0.554.0: resolution: {integrity: sha512-St+z29uthEJVx0Is7ellNkgTEhaeSoA42I7JjOCBCrc5X6LYMGSv0P/2uS5HDLTExP5tpiqRD2PyUEOS6s9UXA==} peerDependencies: @@ -8513,6 +9412,17 @@ packages: resolution: {integrity: sha512-uCbIa8jWWmQZt4dSnEStkVC6gdakiinAm4PiGsywIkguF0eWMdcjDz0ECYhUolFU3pFLOev9VNPCEygydXnddg==} engines: {node: ^20.17.0 || >=22.9.0} + map-limit@0.0.1: + resolution: {integrity: sha512-pJpcfLPnIF/Sk3taPW21G/RQsEEirGaFpCW3oXRwH9dnFHPHNGjNyvh++rdmC2fNqEaTw2MhYJraoJWAHx8kEg==} + + mapbox-gl@1.13.3: + resolution: {integrity: sha512-p8lJFEiqmEQlyv+DQxFAOG/XPWN0Wp7j/Psq93Zywz7qt9CcUKFYDBOoOEKzqe6gudHVJY8/Bhqw6VDpX2lSBg==} + engines: {node: '>=6.4.0'} + + maplibre-gl@4.7.1: + resolution: {integrity: sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==} + engines: {node: '>=16.14.0', npm: '>=8.1.0'} + mark.js@8.11.1: resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} @@ -8544,6 +9454,10 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + math-log2@1.0.1: + resolution: {integrity: sha512-9W0yGtkaMAkf74XGYVy4Dqw3YUMnTNB2eeiw9aQbUl4A3KmuCEHTt2DgAB07ENzOYAjsYSAYufkAq0Zd+jU7zA==} + engines: {node: '>=0.10.0'} + mdast-util-directive@3.1.0: resolution: {integrity: sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==} @@ -8905,6 +9819,18 @@ packages: moment@2.30.1: resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + mouse-change@1.4.0: + resolution: {integrity: sha512-vpN0s+zLL2ykyyUDh+fayu9Xkor5v/zRD9jhSqjRS1cJTGS0+oakVZzNm5n19JvvEj0you+MXlYTpNxUDQUjkQ==} + + mouse-event-offset@3.0.2: + resolution: {integrity: sha512-s9sqOs5B1Ykox3Xo8b3Ss2IQju4UwlW6LSR+Q5FXWpprJ5fzMLefIIItr3PH8RwzfGy6gxs/4GAmiNuZScE25w==} + + mouse-event@1.0.5: + resolution: {integrity: sha512-ItUxtL2IkeSKSp9cyaX2JLUuKk2uMoxBg4bbOWVd29+CskYJR9BGsUqtXenNzKbnDshvupjUewDIYVrOB6NmGw==} + + mouse-wheel@1.2.0: + resolution: {integrity: sha512-+OfYBiUOCTWcTECES49neZwL5AoGkXE+lFjIvzwNCnYRlso+EnfvovcBxGoyQ0yQt806eSPjS675K0EwWknXmw==} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -8923,6 +9849,9 @@ packages: resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} hasBin: true + murmurhash-js@1.0.0: + resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==} + mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} @@ -8935,9 +9864,17 @@ packages: napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + native-promise-only@0.8.1: + resolution: {integrity: sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==} + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + needle@2.9.1: + resolution: {integrity: sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==} + engines: {node: '>= 4.4.x'} + hasBin: true + negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} @@ -8967,6 +9904,9 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + next-tick@1.1.0: + resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} + no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} @@ -9040,6 +9980,16 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + normalize-svg-path@0.1.0: + resolution: {integrity: sha512-1/kmYej2iedi5+ROxkRESL/pI02pkg0OBnaR4hJkSIX6+ORzepwbuUXfrdZaPjysTsJInj0Rj5NuX027+dMBvA==} + + normalize-svg-path@1.1.0: + resolution: {integrity: sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==} + normalize-url@8.1.0: resolution: {integrity: sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==} engines: {node: '>=14.16'} @@ -9087,6 +10037,10 @@ packages: peerDependencies: webpack: ^4.0.0 || ^5.0.0 + number-is-integer@1.0.1: + resolution: {integrity: sha512-Dq3iuiFBkrbmuQjGFFF3zckXNCQoSD37/SdSbgcBailUx6knDvDwb5CympBgcoWHy36sfS12u74MHYkXyHq6bg==} + engines: {node: '>=0.10.0'} + nypm@0.6.2: resolution: {integrity: sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==} engines: {node: ^14.16.0 || >=16.10.0} @@ -9129,6 +10083,9 @@ packages: resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} engines: {node: '>= 0.8'} + once@1.3.3: + resolution: {integrity: sha512-6vaNInhu+CHxtONf3zw3vq4SP2DOQhjBvIa3rNcG0+P7eKWlYH6Peu7rHizSloRU2EwMz6GraLieis9Ac9+p1w==} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -9144,6 +10101,12 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + oniguruma-parser@0.12.2: + resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} + + oniguruma-to-es@4.3.6: + resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + open@10.2.0: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} @@ -9247,6 +10210,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parenthesis@3.1.8: + resolution: {integrity: sha512-KF/U8tk54BgQewkJPvB4s/US3VQY68BRDpH638+7O/n58TpnwiwnOtGIOsT2/i+M78s61BBpeC83STB88d8sqw==} + parse-conflict-json@5.0.1: resolution: {integrity: sha512-ZHEmNKMq1wyJXNwLxyHnluPfRAFSIliBvbK/UiOceROt4Xh9Pz0fq49NytIaeaCUf5VR86hwQ/34FCcNU5/LKQ==} engines: {node: ^20.17.0 || >=22.9.0} @@ -9264,6 +10230,15 @@ packages: parse-path@7.1.0: resolution: {integrity: sha512-EuCycjZtfPcjWk7KTksnJ5xPMvWGA/6i4zrLYhRG0hGvC3GPU/jGUj3Cy+ZR0v30duV3e23R95T1lE2+lsndSw==} + parse-rect@1.2.0: + resolution: {integrity: sha512-4QZ6KYbnE6RTwg9E0HpLchUM9EZt6DnDxajFZZDSV4p/12ZJEvPO702DZpGvRYEPo00yKDys7jASi+/w7aO8LA==} + + parse-svg-path@0.1.2: + resolution: {integrity: sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==} + + parse-unit@1.0.1: + resolution: {integrity: sha512-hrqldJHokR3Qj88EIlV/kAyAi/G5R2+R56TBANxNMy0uPlYcttx0jnMW6Yx5KsKPSbC3KddM/7qQm3+0wEXKxg==} + parse-url@9.2.0: resolution: {integrity: sha512-bCgsFI+GeGWPAvAiUv63ZorMeif3/U0zaXABGJbOWt5OH2KCaPHF6S+0ok4aqM9RuIPGyZdx9tR9l13PsW4AYQ==} engines: {node: '>=14.13.0'} @@ -9340,9 +10315,16 @@ packages: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} + pbf@3.3.0: + resolution: {integrity: sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==} + hasBin: true + perfect-debounce@2.0.0: resolution: {integrity: sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==} + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + pg-cloudflare@1.3.0: resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} @@ -9377,6 +10359,9 @@ packages: pgpass@1.0.5: resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + pick-by-alias@1.2.0: + resolution: {integrity: sha512-ESj2+eBxhGrcA1azgHs7lARG5+5iLakc/6nlfbpjcLl00HuuUOIuORhYXN4D1HfvMSKuVtFQjAlnwi1JHEeDIw==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -9413,12 +10398,22 @@ packages: engines: {node: '>=18'} hasBin: true + plotly.js@3.5.0: + resolution: {integrity: sha512-a3AYQIMG7OdZmrJ/fJ65HSt3g1l5qDeludKqjjafU1dh5E+fwqDhsEBndW7VCYwjlducCfN6KtPdWdiWFcoBWw==} + engines: {node: '>=18.0.0'} + + point-in-polygon@1.1.0: + resolution: {integrity: sha512-3ojrFwjnnw8Q9242TzgXuTD+eKiutbzyslcq1ydfu82Db2y+Ogbmyrkpv0Hgj31qwT3lbS9+QAAO/pIQM35XRw==} + points-on-curve@0.2.0: resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} points-on-path@0.2.1: resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + polybooljs@1.2.2: + resolution: {integrity: sha512-ziHW/02J0XuNuUtmidBc6GXE8YohYydp3DWPWXYsd7O721TjcmN+k6ezjdwkDqep+gnWnFY+yqZHvzElra2oCg==} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -9830,6 +10825,12 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} + potpack@1.0.2: + resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} + + potpack@2.1.0: + resolution: {integrity: sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==} + prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} @@ -9865,6 +10866,9 @@ packages: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} + probe-image-size@7.2.3: + resolution: {integrity: sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w==} + proc-log@6.1.0: resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==} engines: {node: ^20.17.0 || >=22.9.0} @@ -9896,6 +10900,9 @@ packages: resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} engines: {node: '>=12.0.0'} + protocol-buffers-schema@3.6.1: + resolution: {integrity: sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==} + protocols@2.0.2: resolution: {integrity: sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==} @@ -9948,6 +10955,15 @@ packages: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} + quickselect@2.0.0: + resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==} + + quickselect@3.0.0: + resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==} + + raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -10021,6 +11037,24 @@ packages: react-loadable: '*' webpack: '>=4.41.1 || 5.x' + react-plotly.js@2.6.0: + resolution: {integrity: sha512-g93xcyhAVCSt9kV1svqG1clAEdL6k3U+jjuSzfTV7owaSU9Go6Ph8bl25J+jKfKvIGAEYpe4qj++WHJuc9IaeA==} + peerDependencies: + plotly.js: '>1.34.0' + react: '>0.13.0' + + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} @@ -10105,6 +11139,9 @@ packages: resolution: {integrity: sha512-S16VePJnQcfmk6HIZAiP8TXW/VDlDtZfzVndRDE8lhZNA4YvAiwAjgvhoyf6+soofEH/vrZnOUctSt+jYE2tkg==} engines: {node: ^20.17.0 || >=22.9.0} + readable-stream@1.0.34: + resolution: {integrity: sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==} + readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -10120,6 +11157,10 @@ packages: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} + recast@0.23.11: + resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} + engines: {node: '>= 4'} + recharts-scale@0.4.5: resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} @@ -10130,6 +11171,14 @@ packages: react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + recharts@3.4.1: + resolution: {integrity: sha512-35kYg6JoOgwq8sE4rhYkVWwa6aAIgOtT+Ob0gitnShjwUwZmhrmy7Jco/5kJNF4PnLXgt9Hwq+geEMS+WrjU1g==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + recma-build-jsx@1.0.0: resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} @@ -10144,6 +11193,14 @@ packages: recma-stringify@1.0.0: resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -10154,6 +11211,15 @@ packages: regenerate@1.4.2: resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + regexpu-core@6.4.0: resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} engines: {node: '>=4'} @@ -10173,6 +11239,21 @@ packages: resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} hasBin: true + regl-error2d@2.0.12: + resolution: {integrity: sha512-r7BUprZoPO9AbyqM5qlJesrSRkl+hZnVKWKsVp7YhOl/3RIpi4UDGASGJY0puQ96u5fBYw/OlqV24IGcgJ0McA==} + + regl-line2d@3.1.3: + resolution: {integrity: sha512-fkgzW+tTn4QUQLpFKsUIE0sgWdCmXAM3ctXcCgoGBZTSX5FE2A0M7aynz7nrZT5baaftLrk9te54B+MEq4QcSA==} + + regl-scatter2d@3.4.0: + resolution: {integrity: sha512-DavKQlHsI+iHZuLgOL+yGkg+sPd94CS+7FCBWkcQ6s/TbaNfUsF9eN591fjjSWIoKrGNfb/SEGhsXR5lXjqZ2w==} + + regl-splom@1.0.14: + resolution: {integrity: sha512-OiLqjmPRYbd7kDlHC6/zDf6L8lxgDC65BhC8JirhP4ykrK4x22ZyS+BnY8EUinXKDeMgmpRwCvUmk7BK4Nweuw==} + + regl@2.1.1: + resolution: {integrity: sha512-+IOGrxl3FZ8ZM9ixCWQZzFRiRn7Rzn9bu3iFHwg/yz4tlOUQgbO4PHLgG+1ZT60zcIV8tief6Qrmyl8qcoJP0g==} + rehype-minify-whitespace@6.0.2: resolution: {integrity: sha512-Zk0pyQ06A3Lyxhe9vGtOtzz3Z0+qZ5+7icZ/PL/2x1SHPbKao5oB/g/rlc6BCTajqBb33JcOe71Ye1oFsuYbnw==} @@ -10250,6 +11331,9 @@ packages: requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} @@ -10267,6 +11351,12 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve-protobuf-schema@2.1.0: + resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} + + resolve@0.6.3: + resolution: {integrity: sha512-UHBY3viPlJKf85YijDUcikKX6tmF4SokIDp518ZDVT92JNDcG5uKIthaT/owt3Sar0lwtOafsQuwrg22/v2Dwg==} + resolve@1.22.10: resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} engines: {node: '>= 0.4'} @@ -10298,6 +11388,9 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + right-now@1.0.0: + resolution: {integrity: sha512-DA8+YS+sMIVpbsuKgy+Z67L9Lxb1p05mNxRpDPNksPDEFir4vmBlUtuN9jkTGn9YMMdlBuK7XQgFiz6ws+yhSg==} + rimraf@5.0.10: resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} hasBin: true @@ -10527,6 +11620,16 @@ packages: serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + seroval-plugins@1.5.2: + resolution: {integrity: sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.5.2: + resolution: {integrity: sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==} + engines: {node: '>=10'} + serve-handler@6.1.6: resolution: {integrity: sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==} @@ -10557,6 +11660,9 @@ packages: resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} engines: {node: '>=8'} + shallow-copy@0.0.1: + resolution: {integrity: sha512-b6i4ZpVuUxB9h5gfCxPiusKYkqTMOjEbBs4wMaFbkfia4yFv92UKZ6Df8WXcKbn08JNL/abvg3FnMAOfakDvUw==} + shallowequal@1.1.0: resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} @@ -10572,6 +11678,9 @@ packages: resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} engines: {node: '>= 0.4'} + shiki@3.15.0: + resolution: {integrity: sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw==} + side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -10598,6 +11707,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + signum@1.0.0: + resolution: {integrity: sha512-yodFGwcyt59XRh7w5W3jPcIQb3Bwi21suEfT7MAWnBX3iCdklJpgDgvGT9o04UonglZN5SNMfJFkHIR/jO8GHw==} + simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} @@ -10665,6 +11777,9 @@ packages: resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + solid-js@1.9.12: + resolution: {integrity: sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw==} + sonner@2.0.7: resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} peerDependencies: @@ -10737,9 +11852,15 @@ packages: resolution: {integrity: sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==} engines: {node: ^20.17.0 || >=22.9.0} + stack-trace@0.0.9: + resolution: {integrity: sha512-vjUc6sfgtgY0dxCdnc40mK6Oftjo9+2K8H/NG81TMhgL392FtiPA9tn9RLyTxXmTLPJPjF3VyzFp6bsWFLisMQ==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + static-eval@2.1.1: + resolution: {integrity: sha512-MgWpQ/ZjGieSVB3eOJVs4OA2LT/q1vx98KPCTTQPzq/aLr0YUXTsgryTXr4SLfR0ZfUUCiedM9n/ABeDIyy4mA==} + statuses@1.5.0: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} @@ -10759,10 +11880,19 @@ packages: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} + stream-parser@0.3.1: + resolution: {integrity: sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==} + + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} + string-split-by@1.0.0: + resolution: {integrity: sha512-KaJKY+hfpzNyet/emP81PJA9hTVSfxNLS9SFTWxdCnnW1/zOOwiV248+EfoX7IQFcBaOp4G5YE6xTJMF+pLg6A==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -10779,6 +11909,9 @@ packages: resolution: {integrity: sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==} engines: {node: '>=20'} + string_decoder@0.10.31: + resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} + string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} @@ -10827,6 +11960,9 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + strongly-connected-components@1.0.1: + resolution: {integrity: sha512-i0TFx4wPcO0FwX+4RkLJi1MxmcTv90jNZgxMu9XRnMXMeFUY1VJlIoXpZunPUvUUqbCT1pg5PEkFqqpcaElNaA==} + style-to-js@1.1.21: resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} @@ -10842,6 +11978,15 @@ packages: stylis@4.3.6: resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} + supercluster@7.1.5: + resolution: {integrity: sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==} + + supercluster@8.0.1: + resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==} + + superscript-text@1.0.0: + resolution: {integrity: sha512-gwu8l5MtRZ6koO0icVTlmN5pm7Dhh1+Xpe9O4x6ObMAsW+3jPbW14d1DsBq1F4wiI+WOFjXF35pslgec/G8yCQ==} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -10854,9 +11999,18 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svg-arc-to-cubic-bezier@3.2.0: + resolution: {integrity: sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==} + svg-parser@2.0.4: resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} + svg-path-bounds@1.0.2: + resolution: {integrity: sha512-H4/uAgLWrppIC0kHsb2/dWUYSmb4GE5UqH06uqWBcg6LBjX2fu0A8+JrO2/FJPZiSsNOKZAhyFFgsLTdYUvSqQ==} + + svg-path-sdf@1.1.3: + resolution: {integrity: sha512-vJJjVq/R5lSr2KLfVXVAStktfcfa1pNFjFOgyJnzZFXlO/fDZ5DmM8FpnSKKzLPfEYTVeXuVBTHF296TpxuJVg==} + svgo@3.3.2: resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==} engines: {node: '>=14.0.0'} @@ -10878,6 +12032,9 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} + tailwind-merge@3.3.1: + resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + tailwind-merge@3.4.0: resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} @@ -10946,6 +12103,12 @@ packages: resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} engines: {node: '>=18'} + through2@0.6.5: + resolution: {integrity: sha512-RkK/CCESdTKQZHdmKICijdKKsCRVHs5KsLZ6pACAmF/1GPUQhonHSXWNERctxEp7RmvjdNbZTL5z9V7nSCXKcg==} + + through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -10961,6 +12124,9 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} @@ -10976,6 +12142,12 @@ packages: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} + tinyqueue@2.0.3: + resolution: {integrity: sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==} + + tinyqueue@3.0.0: + resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==} + tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} @@ -10995,6 +12167,12 @@ packages: resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} engines: {node: '>= 0.4'} + to-float32@1.1.0: + resolution: {integrity: sha512-keDnAusn/vc+R3iEiSDw8TOF7gPiTLdK1ArvWtYbJQiVfmRg6i/CAvbKq3uIS0vWroAC7ZecN3DjQKw3aSklUg==} + + to-px@1.0.1: + resolution: {integrity: sha512-2y3LjBeIZYL19e5gczp14/uRWFDtDUErJPVN3VU9a7SJO+RjGRtYR47aMN2bZgGlxvW4ZcEz2ddUPVHXcMfuXw==} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -11006,6 +12184,10 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + topojson-client@3.1.0: + resolution: {integrity: sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==} + hasBin: true + toposort-class@1.0.1: resolution: {integrity: sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==} @@ -11174,10 +12356,16 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} + type@2.7.3: + resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} + typedarray-pool@1.2.0: + resolution: {integrity: sha512-YTSQbzX43yvtpfRtIDAYygoYtgT+Rpjuxy9iOpczrjpXLgGoyG7aS5USJXV2d3nn8uHTeb9rXDvzS27zUg5KYQ==} + typedarray-to-buffer@3.1.5: resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} @@ -11257,6 +12445,13 @@ packages: typeorm-aurora-data-api-driver: optional: true + typescript-eslint@8.45.0: + resolution: {integrity: sha512-qzDmZw/Z5beNLUrXfd0HIW6MzIaAV5WNDxmMs9/3ojGOpYavofgNAAD/nC6tGV2PczIi0iw8vot2eAe/sBn7zg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + typescript-eslint@8.49.0: resolution: {integrity: sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -11299,6 +12494,9 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.13.0: + resolution: {integrity: sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==} + undici-types@7.14.0: resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} @@ -11394,6 +12592,13 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + + unquote@1.1.1: + resolution: {integrity: sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==} + unrun@0.2.28: resolution: {integrity: sha512-LqMrI3ZEUMZ2476aCsbUTfy95CHByqez05nju4AQv4XFPkxh5yai7Di1/Qb0FoELHEEPDWhQi23EJeFyrBV0Og==} engines: {node: '>=20.19.0'} @@ -11404,18 +12609,15 @@ packages: synckit: optional: true - update-browserslist-db@1.1.4: - resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - update-browserslist-db@1.2.2: resolution: {integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' + update-diff@1.1.0: + resolution: {integrity: sha512-rCiBPiHxZwT4+sBhEbChzpO5hYHjm91kScWgdHf4Qeafs6Ba7MBl+d9GlGv72bcTZQO0sLmtQS1pHSWoCLtN/A==} + update-notifier@6.0.2: resolution: {integrity: sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==} engines: {node: '>=14.16'} @@ -11537,6 +12739,9 @@ packages: victory-vendor@36.9.2: resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -11638,6 +12843,9 @@ packages: vscode-uri@3.0.8: resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + vt-pbf@3.1.3: + resolution: {integrity: sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==} + w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -11653,6 +12861,9 @@ packages: wbuf@1.7.3: resolution: {integrity: sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==} + weak-map@1.0.8: + resolution: {integrity: sha512-lNR9aAefbGPpHO7AEnY0hCFjz1eTkWCXYvkTRrTHs9qv8zJp+SkVYpzfLIFXQQiG3tVvbNFQgVg2bQS8YGgxyw==} + web-namespaces@1.1.4: resolution: {integrity: sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==} @@ -11663,6 +12874,9 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webgl-context@2.2.0: + resolution: {integrity: sha512-q/fGIivtqTT7PEoF07axFIlHNk/XCPaYpq64btnepopSWvKNFkoORlQYgqDigBIuGA1ExnFd/GnSUnBNEPQY7Q==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -11709,6 +12923,9 @@ packages: resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} engines: {node: '>=10.13.0'} + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + webpack@5.103.0: resolution: {integrity: sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==} engines: {node: '>=10.13.0'} @@ -11758,6 +12975,11 @@ packages: engines: {node: '>= 8'} hasBin: true + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + which@6.0.1: resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} engines: {node: ^20.17.0 || >=22.9.0} @@ -11799,6 +13021,9 @@ packages: resolution: {integrity: sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==} engines: {node: '>=12.17'} + world-calendars@1.0.4: + resolution: {integrity: sha512-VGRnLJS+xJmGDPodgJRnGIDwGu0s+Cr9V2HB3EzlDZ5n0qb8h5SJtGUEkjrphZYAglEiXZ6kiXdmk0H/h/uu/w==} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -11868,6 +13093,10 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xtend@2.2.0: + resolution: {integrity: sha512-SLt5uylT+4aoXxXuwtQp5ZnMMzhDb1Xkg4pEqc00WUJCQifPfV9Ub1VrNhp9kXkrjZD2I2Hl8WnjP37jzZLPZw==} + engines: {node: '>=0.4'} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -11929,6 +13158,9 @@ packages: peerDependencies: zod: ^3.25.0 || ^4.0.0 + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.1.13: resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} @@ -12200,15 +13432,15 @@ snapshots: '@babel/core@7.28.5': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.5 + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) '@babel/helpers': 7.28.4 - '@babel/parser': 7.28.5 - '@babel/template': 7.27.2 - '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -12245,13 +13477,13 @@ snapshots: '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@babel/helper-compilation-targets@7.27.2': dependencies: '@babel/compat-data': 7.28.5 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.0 + browserslist: 4.28.1 lru-cache: 5.1.1 semver: 6.3.1 @@ -12263,7 +13495,7 @@ snapshots: '@babel/helper-optimise-call-expression': 7.27.1 '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.5) '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.28.5 + '@babel/traverse': 7.29.0 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -12290,15 +13522,15 @@ snapshots: '@babel/helper-member-expression-to-functions@7.28.5': dependencies: - '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color @@ -12307,13 +13539,13 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-module-imports': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.28.5 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@babel/helper-plugin-utils@7.27.1': {} @@ -12331,14 +13563,14 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-member-expression-to-functions': 7.28.5 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.28.5 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: - '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color @@ -12364,8 +13596,8 @@ snapshots: '@babel/helpers@7.28.4': dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.5 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 '@babel/parser@7.28.4': dependencies: @@ -13111,6 +14343,10 @@ snapshots: '@chevrotain/utils@11.0.3': {} + '@choojs/findup@0.2.1': + dependencies: + commander: 2.20.3 + '@clack/core@1.0.1': dependencies: picocolors: 1.1.1 @@ -14430,23 +15666,12 @@ snapshots: - uglify-js - webpack-cli - '@emnapi/core@1.7.1': - dependencies: - '@emnapi/wasi-threads': 1.1.0 - tslib: 2.8.1 - optional: true - '@emnapi/core@1.8.1': dependencies: '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.7.1': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.8.1': dependencies: tslib: 2.8.1 @@ -14535,6 +15760,11 @@ snapshots: '@esbuild/win32-x64@0.25.10': optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@9.36.0(jiti@2.6.1))': + dependencies: + eslint: 9.36.0(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@2.6.1))': dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -14550,10 +15780,16 @@ snapshots: transitivePeerDependencies: - supports-color + '@eslint/config-helpers@0.3.1': {} + '@eslint/config-helpers@0.4.2': dependencies: '@eslint/core': 0.17.0 + '@eslint/core@0.15.2': + dependencies: + '@types/json-schema': 7.0.15 + '@eslint/core@0.17.0': dependencies: '@types/json-schema': 7.0.15 @@ -14572,10 +15808,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@eslint/js@9.36.0': {} + '@eslint/js@9.39.1': {} '@eslint/object-schema@2.1.7': {} + '@eslint/plugin-kit@0.3.5': + dependencies: + '@eslint/core': 0.15.2 + levn: 0.4.1 + '@eslint/plugin-kit@0.4.1': dependencies: '@eslint/core': 0.17.0 @@ -14876,6 +16119,45 @@ snapshots: '@leichtgewicht/ip-codec@2.0.5': {} + '@mapbox/geojson-rewind@0.5.2': + dependencies: + get-stream: 6.0.1 + minimist: 1.2.8 + + '@mapbox/geojson-types@1.0.2': {} + + '@mapbox/jsonlint-lines-primitives@2.0.2': {} + + '@mapbox/mapbox-gl-supported@1.5.0(mapbox-gl@1.13.3)': + dependencies: + mapbox-gl: 1.13.3 + + '@mapbox/point-geometry@0.1.0': {} + + '@mapbox/tiny-sdf@1.2.5': {} + + '@mapbox/tiny-sdf@2.1.0': {} + + '@mapbox/unitbezier@0.0.0': {} + + '@mapbox/unitbezier@0.0.1': {} + + '@mapbox/vector-tile@1.3.1': + dependencies: + '@mapbox/point-geometry': 0.1.0 + + '@mapbox/whoots-js@3.1.0': {} + + '@maplibre/maplibre-gl-style-spec@20.4.0': + dependencies: + '@mapbox/jsonlint-lines-primitives': 2.0.2 + '@mapbox/unitbezier': 0.0.1 + json-stringify-pretty-compact: 4.0.0 + minimist: 1.2.8 + quickselect: 2.0.0 + rw: 1.3.3 + tinyqueue: 3.0.0 + '@mdx-js/mdx@3.1.1': dependencies: '@types/estree': 1.0.8 @@ -14916,13 +16198,6 @@ snapshots: dependencies: langium: 3.3.1 - '@napi-rs/wasm-runtime@1.0.7': - dependencies: - '@emnapi/core': 1.7.1 - '@emnapi/runtime': 1.7.1 - '@tybys/wasm-util': 0.10.1 - optional: true - '@napi-rs/wasm-runtime@1.1.1': dependencies: '@emnapi/core': 1.8.1 @@ -15829,6 +17104,63 @@ snapshots: dependencies: playwright: 1.58.1 + '@plotly/d3-sankey-circular@0.33.1': + dependencies: + d3-array: 1.2.4 + d3-collection: 1.0.7 + d3-shape: 1.3.7 + elementary-circuits-directed-graph: 1.3.1 + + '@plotly/d3-sankey@0.7.2': + dependencies: + d3-array: 1.2.4 + d3-collection: 1.0.7 + d3-shape: 1.3.7 + + '@plotly/d3@3.8.2': {} + + '@plotly/mapbox-gl@1.13.4(mapbox-gl@1.13.3)': + dependencies: + '@mapbox/geojson-rewind': 0.5.2 + '@mapbox/geojson-types': 1.0.2 + '@mapbox/jsonlint-lines-primitives': 2.0.2 + '@mapbox/mapbox-gl-supported': 1.5.0(mapbox-gl@1.13.3) + '@mapbox/point-geometry': 0.1.0 + '@mapbox/tiny-sdf': 1.2.5 + '@mapbox/unitbezier': 0.0.0 + '@mapbox/vector-tile': 1.3.1 + '@mapbox/whoots-js': 3.1.0 + csscolorparser: 1.0.3 + earcut: 2.2.4 + geojson-vt: 3.2.1 + gl-matrix: 3.4.4 + grid-index: 1.1.0 + murmurhash-js: 1.0.0 + pbf: 3.3.0 + potpack: 1.0.2 + quickselect: 2.0.0 + rw: 1.3.3 + supercluster: 7.1.5 + tinyqueue: 2.0.3 + vt-pbf: 3.1.3 + transitivePeerDependencies: + - mapbox-gl + + '@plotly/point-cluster@3.1.9': + dependencies: + array-bounds: 1.0.1 + binary-search-bounds: 2.0.5 + clamp: 1.0.1 + defined: 1.0.1 + dtype: 2.0.0 + flatten-vertex-data: 1.0.2 + is-obj: 1.0.1 + math-log2: 1.0.1 + parse-rect: 1.2.0 + pick-by-alias: 1.2.0 + + '@plotly/regl@2.1.2': {} + '@pnpm/config.env-replace@1.1.0': {} '@pnpm/network.ca-file@1.0.2': @@ -16547,6 +17879,18 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.2)(react@19.2.0)(redux@5.0.1))(react@19.2.0)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@standard-schema/utils': 0.3.0 + immer: 11.1.4 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 19.2.0 + react-redux: 9.2.0(@types/react@19.2.2)(react@19.2.0)(redux@5.0.1) + '@release-it/conventional-changelog@10.0.4(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.2.1)(release-it@19.2.0(@types/node@24.7.2)(magicast@0.3.5))': dependencies: '@conventional-changelog/git-client': 2.5.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.2.1) @@ -16653,7 +17997,7 @@ snapshots: '@rolldown/binding-wasm32-wasi@1.0.0-beta.41': dependencies: - '@napi-rs/wasm-runtime': 1.0.7 + '@napi-rs/wasm-runtime': 1.1.1 optional: true '@rolldown/binding-wasm32-wasi@1.0.0-rc.3': @@ -16765,19 +18109,50 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} + '@shikijs/core@3.15.0': + dependencies: + '@shikijs/types': 3.15.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@3.15.0': + dependencies: + '@shikijs/types': 3.15.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.6 + + '@shikijs/engine-oniguruma@3.15.0': + dependencies: + '@shikijs/types': 3.15.0 + '@shikijs/vscode-textmate': 10.0.2 + '@shikijs/engine-oniguruma@3.20.0': dependencies: '@shikijs/types': 3.20.0 '@shikijs/vscode-textmate': 10.0.2 + '@shikijs/langs@3.15.0': + dependencies: + '@shikijs/types': 3.15.0 + '@shikijs/langs@3.20.0': dependencies: '@shikijs/types': 3.20.0 + '@shikijs/themes@3.15.0': + dependencies: + '@shikijs/types': 3.15.0 + '@shikijs/themes@3.20.0': dependencies: '@shikijs/types': 3.20.0 + '@shikijs/types@3.15.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + '@shikijs/types@3.20.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 @@ -16848,6 +18223,8 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@standard-schema/utils@0.3.0': {} + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -16949,6 +18326,16 @@ snapshots: dependencies: defer-to-connect: 2.0.1 + '@tailwindcss/node@4.1.17': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.17 + '@tailwindcss/node@4.1.18': dependencies: '@jridgewell/remapping': 2.3.5 @@ -16959,42 +18346,93 @@ snapshots: source-map-js: 1.2.1 tailwindcss: 4.1.18 + '@tailwindcss/oxide-android-arm64@4.1.17': + optional: true + '@tailwindcss/oxide-android-arm64@4.1.18': optional: true + '@tailwindcss/oxide-darwin-arm64@4.1.17': + optional: true + '@tailwindcss/oxide-darwin-arm64@4.1.18': optional: true + '@tailwindcss/oxide-darwin-x64@4.1.17': + optional: true + '@tailwindcss/oxide-darwin-x64@4.1.18': optional: true + '@tailwindcss/oxide-freebsd-x64@4.1.17': + optional: true + '@tailwindcss/oxide-freebsd-x64@4.1.18': optional: true + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': + optional: true + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': optional: true + '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': + optional: true + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': optional: true + '@tailwindcss/oxide-linux-arm64-musl@4.1.17': + optional: true + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': optional: true + '@tailwindcss/oxide-linux-x64-gnu@4.1.17': + optional: true + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': optional: true + '@tailwindcss/oxide-linux-x64-musl@4.1.17': + optional: true + '@tailwindcss/oxide-linux-x64-musl@4.1.18': optional: true + '@tailwindcss/oxide-wasm32-wasi@4.1.17': + optional: true + '@tailwindcss/oxide-wasm32-wasi@4.1.18': optional: true + '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': + optional: true + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': optional: true + '@tailwindcss/oxide-win32-x64-msvc@4.1.17': + optional: true + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': optional: true + '@tailwindcss/oxide@4.1.17': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.17 + '@tailwindcss/oxide-darwin-arm64': 4.1.17 + '@tailwindcss/oxide-darwin-x64': 4.1.17 + '@tailwindcss/oxide-freebsd-x64': 4.1.17 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.17 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.17 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.17 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.17 + '@tailwindcss/oxide-linux-x64-musl': 4.1.17 + '@tailwindcss/oxide-wasm32-wasi': 4.1.17 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.17 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.17 + '@tailwindcss/oxide@4.1.18': optionalDependencies: '@tailwindcss/oxide-android-arm64': 4.1.18 @@ -17010,6 +18448,14 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + '@tailwindcss/postcss@4.1.17': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.1.17 + '@tailwindcss/oxide': 4.1.17 + postcss: 8.5.6 + tailwindcss: 4.1.17 + '@tailwindcss/postcss@4.1.18': dependencies: '@alloc/quick-lru': 5.2.0 @@ -17018,22 +18464,160 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.18 - '@tanstack/react-table@8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@tanstack/history@1.133.19': {} + + '@tanstack/react-router-devtools@1.133.22(@tanstack/react-router@1.133.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.133.20)(@types/node@24.6.0)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.12)(terser@5.44.1)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.2)': dependencies: - '@tanstack/table-core': 8.21.3 + '@tanstack/react-router': 1.133.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@tanstack/router-devtools-core': 1.133.22(@tanstack/router-core@1.133.20)(@types/node@24.6.0)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(solid-js@1.9.12)(terser@5.44.1)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.2) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + vite: 7.2.4(@types/node@24.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2) + transitivePeerDependencies: + - '@tanstack/router-core' + - '@types/node' + - csstype + - jiti + - less + - lightningcss + - sass + - sass-embedded + - solid-js + - stylus + - sugarss + - terser + - tiny-invariant + - tsx + - yaml - '@tanstack/table-core@8.21.3': {} + '@tanstack/react-router@1.133.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@tanstack/history': 1.133.19 + '@tanstack/react-store': 0.7.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@tanstack/router-core': 1.133.20 + isbot: 5.1.39 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 - '@testing-library/dom@10.4.1': + '@tanstack/react-store@0.7.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.28.6 - '@types/aria-query': 5.0.4 - aria-query: 5.3.0 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 + '@tanstack/store': 0.7.7 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + use-sync-external-store: 1.6.0(react@19.2.0) + + '@tanstack/react-table@8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@tanstack/table-core': 8.21.3 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@tanstack/router-cli@1.133.20': + dependencies: + '@tanstack/router-generator': 1.133.20 + chokidar: 3.6.0 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + + '@tanstack/router-core@1.133.20': + dependencies: + '@tanstack/history': 1.133.19 + '@tanstack/store': 0.7.7 + cookie-es: 2.0.1 + seroval: 1.5.2 + seroval-plugins: 1.5.2(seroval@1.5.2) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + '@tanstack/router-devtools-core@1.133.22(@tanstack/router-core@1.133.20)(@types/node@24.6.0)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(solid-js@1.9.12)(terser@5.44.1)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.2)': + dependencies: + '@tanstack/router-core': 1.133.20 + clsx: 2.1.1 + goober: 2.1.18(csstype@3.2.3) + solid-js: 1.9.12 + tiny-invariant: 1.3.3 + vite: 7.2.4(@types/node@24.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2) + optionalDependencies: + csstype: 3.2.3 + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + + '@tanstack/router-generator@1.133.20': + dependencies: + '@tanstack/router-core': 1.133.20 + '@tanstack/router-utils': 1.133.19 + '@tanstack/virtual-file-routes': 1.133.19 + prettier: 3.8.1 + recast: 0.23.11 + source-map: 0.7.6 + tsx: 4.20.6 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@tanstack/router-plugin@1.133.22(@tanstack/react-router@1.133.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(rolldown-vite@7.1.14(@types/node@24.6.0)(esbuild@0.25.10)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2))(webpack@5.103.0(esbuild@0.25.10))': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@tanstack/router-core': 1.133.20 + '@tanstack/router-generator': 1.133.20 + '@tanstack/router-utils': 1.133.19 + '@tanstack/virtual-file-routes': 1.133.19 + babel-dead-code-elimination: 1.0.12 + chokidar: 3.6.0 + unplugin: 2.3.11 + zod: 3.25.76 + optionalDependencies: + '@tanstack/react-router': 1.133.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + vite: rolldown-vite@7.1.14(@types/node@24.6.0)(esbuild@0.25.10)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2) + webpack: 5.103.0(esbuild@0.25.10) + transitivePeerDependencies: + - supports-color + + '@tanstack/router-utils@1.133.19': + dependencies: + '@babel/core': 7.28.5 + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.0 + '@babel/preset-typescript': 7.28.5(@babel/core@7.28.5) + ansis: 4.2.0 + diff: 8.0.4 + pathe: 2.0.3 + tinyglobby: 0.2.15 + transitivePeerDependencies: + - supports-color + + '@tanstack/store@0.7.7': {} + + '@tanstack/table-core@8.21.3': {} + + '@tanstack/virtual-file-routes@1.133.19': {} + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/runtime': 7.28.6 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 picocolors: 1.1.1 pretty-format: 27.5.1 @@ -17051,6 +18635,38 @@ snapshots: '@trysound/sax@0.2.0': {} + '@turf/area@7.3.5': + dependencies: + '@turf/helpers': 7.3.5 + '@turf/meta': 7.3.5 + '@types/geojson': 7946.0.16 + tslib: 2.8.1 + + '@turf/bbox@7.3.5': + dependencies: + '@turf/helpers': 7.3.5 + '@turf/meta': 7.3.5 + '@types/geojson': 7946.0.16 + tslib: 2.8.1 + + '@turf/centroid@7.3.5': + dependencies: + '@turf/helpers': 7.3.5 + '@turf/meta': 7.3.5 + '@types/geojson': 7946.0.16 + tslib: 2.8.1 + + '@turf/helpers@7.3.5': + dependencies: + '@types/geojson': 7946.0.16 + tslib: 2.8.1 + + '@turf/meta@7.3.5': + dependencies: + '@turf/helpers': 7.3.5 + '@types/geojson': 7946.0.16 + tslib: 2.8.1 + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -17062,24 +18678,24 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.29.0 '@types/body-parser@1.19.6': dependencies: @@ -17275,6 +18891,10 @@ snapshots: '@types/qs': 6.14.0 '@types/serve-static': 1.15.10 + '@types/geojson-vt@3.2.5': + dependencies: + '@types/geojson': 7946.0.16 + '@types/geojson@7946.0.16': {} '@types/gtag.js@0.0.12': {} @@ -17315,6 +18935,14 @@ snapshots: '@types/lodash@4.17.24': {} + '@types/mapbox__point-geometry@0.1.4': {} + + '@types/mapbox__vector-tile@1.3.4': + dependencies: + '@types/geojson': 7946.0.16 + '@types/mapbox__point-geometry': 0.1.4 + '@types/pbf': 3.0.5 + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -17351,6 +18979,10 @@ snapshots: dependencies: undici-types: 7.16.0 + '@types/node@24.6.0': + dependencies: + undici-types: 7.13.0 + '@types/node@24.7.2': dependencies: undici-types: 7.14.0 @@ -17371,6 +19003,8 @@ snapshots: '@types/parse5@5.0.3': {} + '@types/pbf@3.0.5': {} + '@types/pg-pool@2.0.6': dependencies: '@types/pg': 8.16.0 @@ -17387,6 +19021,8 @@ snapshots: pg-protocol: 1.11.0 pg-types: 2.2.0 + '@types/plotly.js@3.0.10': {} + '@types/prismjs@1.26.5': {} '@types/qs@6.14.0': {} @@ -17401,6 +19037,11 @@ snapshots: dependencies: '@types/react': 19.2.7 + '@types/react-plotly.js@2.6.4': + dependencies: + '@types/plotly.js': 3.0.10 + '@types/react': 19.2.2 + '@types/react-router-config@5.0.11': dependencies: '@types/history': 4.7.11 @@ -17457,6 +19098,10 @@ snapshots: dependencies: '@types/node': 25.2.3 + '@types/supercluster@7.1.3': + dependencies: + '@types/geojson': 7946.0.16 + '@types/tedious@4.0.14': dependencies: '@types/node': 25.2.3 @@ -17468,6 +19113,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/use-sync-external-store@0.0.6': {} + '@types/validator@13.15.10': {} '@types/ws@8.18.1': @@ -17480,6 +19127,23 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 + '@typescript-eslint/eslint-plugin@8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/type-utils': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.45.0 + eslint: 9.36.0(jiti@2.6.1) + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -17496,6 +19160,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.45.0 + debug: 4.4.3 + eslint: 9.36.0(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.49.0 @@ -17508,6 +19184,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.45.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) + '@typescript-eslint/types': 8.49.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/project-service@8.49.0(typescript@5.9.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) @@ -17517,15 +19202,36 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/scope-manager@8.45.0': + dependencies: + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/visitor-keys': 8.45.0 + '@typescript-eslint/scope-manager@8.49.0': dependencies: '@typescript-eslint/types': 8.49.0 '@typescript-eslint/visitor-keys': 8.49.0 + '@typescript-eslint/tsconfig-utils@8.45.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + '@typescript-eslint/tsconfig-utils@8.49.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 + '@typescript-eslint/type-utils@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.36.0(jiti@2.6.1) + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/type-utils@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.49.0 @@ -17538,8 +19244,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/types@8.45.0': {} + '@typescript-eslint/types@8.49.0': {} + '@typescript-eslint/typescript-estree@8.45.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.45.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.45.0(typescript@5.9.3) + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/visitor-keys': 8.45.0 + debug: 4.4.3 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.4 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/typescript-estree@8.49.0(typescript@5.9.3)': dependencies: '@typescript-eslint/project-service': 8.49.0(typescript@5.9.3) @@ -17555,6 +19279,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) + eslint: 9.36.0(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) @@ -17566,6 +19301,11 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/visitor-keys@8.45.0': + dependencies: + '@typescript-eslint/types': 8.45.0 + eslint-visitor-keys: 4.2.1 + '@typescript-eslint/visitor-keys@8.49.0': dependencies: '@typescript-eslint/types': 8.49.0 @@ -17575,6 +19315,18 @@ snapshots: '@vercel/oidc@3.0.5': {} + '@vitejs/plugin-react@5.0.4(rolldown-vite@7.1.14(@types/node@24.6.0)(esbuild@0.25.10)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2))': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) + '@rolldown/pluginutils': 1.0.0-beta.38 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: rolldown-vite@7.1.14(@types/node@24.6.0)(esbuild@0.25.10)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + '@vitejs/plugin-react@5.0.4(vite@7.2.4(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.5 @@ -17759,6 +19511,8 @@ snapshots: abbrev@4.0.0: optional: true + abs-svg-path@0.1.1: {} + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -17780,6 +19534,8 @@ snapshots: dependencies: acorn: 8.15.0 + acorn@7.4.1: {} + acorn@8.15.0: {} address@1.2.2: {} @@ -17799,9 +19555,9 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 4.3.6 - ajv-formats@2.1.1(ajv@8.17.1): + ajv-formats@2.1.1(ajv@8.18.0): optionalDependencies: - ajv: 8.17.1 + ajv: 8.18.0 ajv-formats@3.0.1(ajv@8.17.1): optionalDependencies: @@ -17815,9 +19571,9 @@ snapshots: dependencies: ajv: 6.12.6 - ajv-keywords@5.1.0(ajv@8.17.1): + ajv-keywords@5.1.0(ajv@8.18.0): dependencies: - ajv: 8.17.1 + ajv: 8.18.0 fast-deep-equal: 3.1.3 ajv@6.12.6: @@ -17932,10 +19688,20 @@ snapshots: array-back@6.2.2: {} + array-bounds@1.0.1: {} + + array-find-index@1.0.2: {} + array-flatten@1.1.1: {} array-ify@1.0.0: {} + array-normalize@1.1.4: + dependencies: + array-bounds: 1.0.1 + + array-range@1.0.1: {} + array-union@2.1.0: {} assertion-error@2.0.1: {} @@ -17950,6 +19716,10 @@ snapshots: dependencies: tslib: 2.8.1 + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + astral-regex@2.0.0: {} astring@1.9.0: {} @@ -17962,6 +19732,16 @@ snapshots: dependencies: immediate: 3.3.0 + autoprefixer@10.4.21(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + caniuse-lite: 1.0.30001760 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + autoprefixer@10.4.23(postcss@8.5.6): dependencies: browserslist: 4.28.1 @@ -17975,6 +19755,15 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + babel-dead-code-elimination@1.0.12: + dependencies: + '@babel/core': 7.28.5 + '@babel/parser': 7.29.0 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + babel-loader@9.2.1(@babel/core@7.28.5)(webpack@5.103.0): dependencies: '@babel/core': 7.28.5 @@ -18018,9 +19807,9 @@ snapshots: balanced-match@4.0.4: {} - base64-js@1.5.1: {} + base64-arraybuffer@1.0.2: {} - baseline-browser-mapping@2.8.32: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.9.7: {} @@ -18052,8 +19841,19 @@ snapshots: binary-extensions@2.3.0: {} + binary-search-bounds@2.0.5: {} + birpc@4.0.0: {} + bit-twiddle@1.0.2: {} + + bitmap-sdf@1.0.4: {} + + bl@2.2.1: + dependencies: + readable-stream: 2.3.8 + safe-buffer: 5.2.1 + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -18139,14 +19939,6 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.0: - dependencies: - baseline-browser-mapping: 2.8.32 - caniuse-lite: 1.0.30001757 - electron-to-chromium: 1.5.262 - node-releases: 2.0.27 - update-browserslist-db: 1.1.4(browserslist@4.28.0) - browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.9.7 @@ -18275,10 +20067,12 @@ snapshots: lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 - caniuse-lite@1.0.30001757: {} - caniuse-lite@1.0.30001760: {} + canvas-fit@1.5.0: + dependencies: + element-size: 1.1.1 + ccount@2.0.1: {} chai@5.3.3: @@ -18394,6 +20188,8 @@ snapshots: cjs-module-lexer@1.4.3: {} + clamp@1.0.1: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -18461,12 +20257,46 @@ snapshots: collapse-white-space@2.1.0: {} + color-alpha@1.0.4: + dependencies: + color-parse: 1.4.3 + color-convert@2.0.1: dependencies: color-name: 1.1.4 + color-id@1.1.0: + dependencies: + clamp: 1.0.1 + color-name@1.1.4: {} + color-normalize@1.5.0: + dependencies: + clamp: 1.0.1 + color-rgba: 2.4.0 + dtype: 2.0.0 + + color-parse@1.4.3: + dependencies: + color-name: 1.1.4 + + color-parse@2.0.0: + dependencies: + color-name: 1.1.4 + + color-rgba@2.4.0: + dependencies: + color-parse: 1.4.3 + color-space: 2.3.2 + + color-rgba@3.0.0: + dependencies: + color-parse: 2.0.0 + color-space: 2.3.2 + + color-space@2.3.2: {} + color-support@1.1.3: {} colord@2.9.3: {} @@ -18534,6 +20364,13 @@ snapshots: concat-map@0.0.1: {} + concat-stream@1.6.2: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 2.3.8 + typedarray: 0.0.6 + concat-stream@2.0.0: dependencies: buffer-from: 1.1.2 @@ -18639,6 +20476,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie-es@2.0.1: {} + cookie-signature@1.0.7: {} cookie@0.7.2: {} @@ -18696,6 +20535,8 @@ snapshots: optionalDependencies: typescript: 5.9.3 + country-regex@1.1.0: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -18715,6 +20556,28 @@ snapshots: dependencies: postcss: 8.5.6 + css-font-size-keywords@1.0.0: {} + + css-font-stretch-keywords@1.0.1: {} + + css-font-style-keywords@1.0.1: {} + + css-font-weight-keywords@1.0.0: {} + + css-font@1.2.0: + dependencies: + css-font-size-keywords: 1.0.0 + css-font-stretch-keywords: 1.0.1 + css-font-style-keywords: 1.0.1 + css-font-weight-keywords: 1.0.0 + css-global-keywords: 1.0.1 + css-system-font-keywords: 1.0.0 + pick-by-alias: 1.2.0 + string-split-by: 1.0.0 + unquote: 1.1.1 + + css-global-keywords@1.0.1: {} + css-has-pseudo@7.0.3(postcss@8.5.6): dependencies: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.1) @@ -18772,6 +20635,8 @@ snapshots: css-selector-parser@3.3.0: {} + css-system-font-keywords@1.0.0: {} + css-tree@2.2.1: dependencies: mdn-data: 2.0.28 @@ -18789,6 +20654,8 @@ snapshots: css-what@6.2.2: {} + csscolorparser@1.0.3: {} + cssdb@8.5.2: {} cssesc@3.0.0: {} @@ -18874,6 +20741,8 @@ snapshots: cytoscape@3.33.1: {} + d3-array@1.2.4: {} + d3-array@2.12.1: dependencies: internmap: 1.0.1 @@ -18896,6 +20765,8 @@ snapshots: dependencies: d3-path: 3.1.0 + d3-collection@1.0.7: {} + d3-color@3.1.0: {} d3-contour@4.0.2: @@ -18906,6 +20777,8 @@ snapshots: dependencies: delaunator: 5.0.1 + d3-dispatch@1.0.6: {} + d3-dispatch@3.0.1: {} d3-drag@3.0.0: @@ -18925,18 +20798,40 @@ snapshots: dependencies: d3-dsv: 3.0.1 + d3-force@1.2.1: + dependencies: + d3-collection: 1.0.7 + d3-dispatch: 1.0.6 + d3-quadtree: 1.0.7 + d3-timer: 1.0.10 + d3-force@3.0.0: dependencies: d3-dispatch: 3.0.1 d3-quadtree: 3.0.1 d3-timer: 3.0.1 + d3-format@1.4.5: {} + d3-format@3.1.0: {} + d3-geo-projection@2.9.0: + dependencies: + commander: 2.20.3 + d3-array: 1.2.4 + d3-geo: 1.12.1 + resolve: 1.22.10 + + d3-geo@1.12.1: + dependencies: + d3-array: 1.2.4 + d3-geo@3.1.1: dependencies: d3-array: 3.2.4 + d3-hierarchy@1.1.9: {} + d3-hierarchy@3.1.2: {} d3-interpolate@3.0.1: @@ -18949,6 +20844,8 @@ snapshots: d3-polygon@3.0.1: {} + d3-quadtree@1.0.7: {} + d3-quadtree@3.0.1: {} d3-random@3.0.1: {} @@ -18981,14 +20878,22 @@ snapshots: dependencies: d3-path: 3.1.0 + d3-time-format@2.3.0: + dependencies: + d3-time: 1.1.0 + d3-time-format@4.1.0: dependencies: d3-time: 3.1.0 + d3-time@1.1.0: {} + d3-time@3.1.0: dependencies: d3-array: 3.2.4 + d3-timer@1.0.10: {} + d3-timer@3.0.1: {} d3-transition@3.0.1(d3-selection@3.0.0): @@ -19041,6 +20946,11 @@ snapshots: d3-transition: 3.0.1(d3-selection@3.0.0) d3-zoom: 3.0.0 + d@1.0.2: + dependencies: + es5-ext: 0.10.64 + type: 2.7.3 + dagre-d3-es@7.0.13: dependencies: d3: 7.9.0 @@ -19069,6 +20979,10 @@ snapshots: dependencies: ms: 2.0.0 + debug@3.2.7: + dependencies: + ms: 2.1.3 + debug@4.4.3: dependencies: ms: 2.1.3 @@ -19124,6 +21038,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + defined@1.0.1: {} + defu@6.1.4: {} degenerator@5.0.1: @@ -19146,6 +21062,8 @@ snapshots: destroy@1.2.0: {} + detect-kerning@2.1.2: {} + detect-libc@2.1.2: {} detect-node-es@1.1.0: {} @@ -19163,6 +21081,8 @@ snapshots: dependencies: dequal: 2.0.3 + diff@8.0.4: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -19268,6 +21188,11 @@ snapshots: dottie@2.0.6: {} + draw-svg-path@1.0.0: + dependencies: + abs-svg-path: 0.1.1 + normalize-svg-path: 0.1.0 + drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(pg@8.18.0): optionalDependencies: '@opentelemetry/api': 1.9.0 @@ -19278,14 +21203,29 @@ snapshots: optionalDependencies: oxc-resolver: 11.19.1 + dtype@2.0.0: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 es-errors: 1.3.0 gopd: 1.2.0 + dup@1.0.0: {} + duplexer@0.1.2: {} + duplexify@3.7.1: + dependencies: + end-of-stream: 1.4.5 + inherits: 2.0.4 + readable-stream: 2.3.8 + stream-shift: 1.0.3 + + earcut@2.2.4: {} + + earcut@3.0.2: {} + eastasianwidth@0.2.0: {} ecdsa-sig-formatter@1.0.11: @@ -19308,10 +21248,14 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.262: {} - electron-to-chromium@1.5.267: {} + element-size@1.1.1: {} + + elementary-circuits-directed-graph@1.3.1: + dependencies: + strongly-connected-components: 1.0.1 + embla-carousel-react@8.6.0(react@19.2.0): dependencies: embla-carousel: 8.6.0 @@ -19350,7 +21294,6 @@ snapshots: end-of-stream@1.4.5: dependencies: once: 1.4.0 - optional: true enhanced-resolve@5.18.3: dependencies: @@ -19383,6 +21326,33 @@ snapshots: dependencies: es-errors: 1.3.0 + es-toolkit@1.46.0: {} + + es5-ext@0.10.64: + dependencies: + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + esniff: 2.0.1 + next-tick: 1.1.0 + + es6-iterator@2.0.3: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-symbol: 3.1.4 + + es6-symbol@3.1.4: + dependencies: + d: 1.0.2 + ext: 1.7.0 + + es6-weak-map@2.0.3: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + esast-util-from-estree@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 @@ -19446,6 +21416,10 @@ snapshots: optionalDependencies: source-map: 0.6.1 + eslint-plugin-react-hooks@5.2.0(eslint@9.36.0(jiti@2.6.1)): + dependencies: + eslint: 9.36.0(jiti@2.6.1) + eslint-plugin-react-hooks@7.0.1(eslint@9.39.1(jiti@2.6.1)): dependencies: '@babel/core': 7.28.5 @@ -19457,6 +21431,10 @@ snapshots: transitivePeerDependencies: - supports-color + eslint-plugin-react-refresh@0.4.22(eslint@9.36.0(jiti@2.6.1)): + dependencies: + eslint: 9.36.0(jiti@2.6.1) + eslint-plugin-react-refresh@0.4.24(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -19475,6 +21453,48 @@ snapshots: eslint-visitor-keys@4.2.1: {} + eslint@9.36.0(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.3.1 + '@eslint/core': 0.15.2 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.36.0 + '@eslint/plugin-kit': 0.3.5 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + eslint@9.39.1(jiti@2.6.1): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) @@ -19516,6 +21536,13 @@ snapshots: transitivePeerDependencies: - supports-color + esniff@2.0.1: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + event-emitter: 0.3.5 + type: 2.7.3 + espree@10.4.0: dependencies: acorn: 8.15.0 @@ -19586,6 +21613,11 @@ snapshots: '@types/node': 25.2.3 require-like: 0.1.2 + event-emitter@0.3.5: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + eventemitter3@4.0.7: {} eventemitter3@5.0.1: {} @@ -19664,12 +21696,21 @@ snapshots: exsolve@1.0.8: {} + ext@1.7.0: + dependencies: + type: 2.7.3 + extend-shallow@2.0.1: dependencies: is-extendable: 0.1.1 extend@3.0.2: {} + falafel@2.2.5: + dependencies: + acorn: 7.4.1 + isarray: 2.0.5 + fast-content-type-parse@3.0.0: {} fast-deep-equal@3.1.3: {} @@ -19684,6 +21725,10 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-isnumeric@1.1.4: + dependencies: + is-string-blank: 1.0.1 + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} @@ -19796,8 +21841,20 @@ snapshots: flatted@3.3.3: {} + flatten-vertex-data@1.0.2: + dependencies: + dtype: 2.0.0 + follow-redirects@1.15.11: {} + font-atlas@2.1.0: + dependencies: + css-font: 1.2.0 + + font-measure@1.2.2: + dependencies: + css-font: 1.2.0 + for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -19825,10 +21882,17 @@ snapshots: forwarded@0.2.0: {} + fraction.js@4.3.7: {} + fraction.js@5.3.4: {} fresh@0.5.2: {} + from2@2.3.0: + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + fs-constants@1.0.0: optional: true @@ -19902,8 +21966,14 @@ snapshots: gensync@1.0.0-beta.2: {} + geojson-vt@3.2.1: {} + + geojson-vt@4.0.2: {} + get-caller-file@2.0.5: {} + get-canvas-context@1.0.2: {} + get-east-asian-width@1.4.0: {} get-intrinsic@1.3.0: @@ -19978,6 +22048,40 @@ snapshots: github-slugger@1.5.0: {} + gl-mat4@1.2.0: {} + + gl-matrix@3.4.4: {} + + gl-text@1.4.0: + dependencies: + bit-twiddle: 1.0.2 + color-normalize: 1.5.0 + css-font: 1.2.0 + detect-kerning: 2.1.2 + es6-weak-map: 2.0.3 + flatten-vertex-data: 1.0.2 + font-atlas: 2.1.0 + font-measure: 1.2.2 + gl-util: 3.1.3 + is-plain-obj: 1.1.0 + object-assign: 4.1.1 + parse-rect: 1.2.0 + parse-unit: 1.0.1 + pick-by-alias: 1.2.0 + regl: 2.1.1 + to-px: 1.0.1 + typedarray-pool: 1.2.0 + + gl-util@3.1.3: + dependencies: + is-browser: 2.1.0 + is-firefox: 1.0.3 + is-plain-obj: 1.1.0 + number-is-integer: 1.0.1 + object-assign: 4.1.1 + pick-by-alias: 1.2.0 + weak-map: 1.0.8 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -20024,8 +22128,16 @@ snapshots: dependencies: ini: 2.0.0 + global-prefix@4.0.0: + dependencies: + ini: 4.1.3 + kind-of: 6.0.3 + which: 4.0.0 + globals@14.0.0: {} + globals@16.4.0: {} + globals@16.5.0: {} globby@11.1.0: @@ -20047,6 +22159,92 @@ snapshots: globrex@0.1.2: {} + glsl-inject-defines@1.0.3: + dependencies: + glsl-token-inject-block: 1.1.0 + glsl-token-string: 1.0.1 + glsl-tokenizer: 2.1.5 + + glsl-resolve@0.0.1: + dependencies: + resolve: 0.6.3 + xtend: 2.2.0 + + glsl-token-assignments@2.0.2: {} + + glsl-token-defines@1.0.0: + dependencies: + glsl-tokenizer: 2.1.5 + + glsl-token-depth@1.1.2: {} + + glsl-token-descope@1.0.2: + dependencies: + glsl-token-assignments: 2.0.2 + glsl-token-depth: 1.1.2 + glsl-token-properties: 1.0.1 + glsl-token-scope: 1.1.2 + + glsl-token-inject-block@1.1.0: {} + + glsl-token-properties@1.0.1: {} + + glsl-token-scope@1.1.2: {} + + glsl-token-string@1.0.1: {} + + glsl-token-whitespace-trim@1.0.0: {} + + glsl-tokenizer@2.1.5: + dependencies: + through2: 0.6.5 + + glslify-bundle@5.1.1: + dependencies: + glsl-inject-defines: 1.0.3 + glsl-token-defines: 1.0.0 + glsl-token-depth: 1.1.2 + glsl-token-descope: 1.0.2 + glsl-token-scope: 1.1.2 + glsl-token-string: 1.0.1 + glsl-token-whitespace-trim: 1.0.0 + glsl-tokenizer: 2.1.5 + murmurhash-js: 1.0.0 + shallow-copy: 0.0.1 + + glslify-deps@1.3.2: + dependencies: + '@choojs/findup': 0.2.1 + events: 3.3.0 + glsl-resolve: 0.0.1 + glsl-tokenizer: 2.1.5 + graceful-fs: 4.2.11 + inherits: 2.0.4 + map-limit: 0.0.1 + resolve: 1.22.10 + + glslify@7.1.1: + dependencies: + bl: 2.2.1 + concat-stream: 1.6.2 + duplexify: 3.7.1 + falafel: 2.2.5 + from2: 2.3.0 + glsl-resolve: 0.0.1 + glsl-token-whitespace-trim: 1.0.0 + glslify-bundle: 5.1.1 + glslify-deps: 1.3.2 + minimist: 1.2.8 + resolve: 1.22.10 + stack-trace: 0.0.9 + static-eval: 2.1.1 + through2: 2.0.5 + xtend: 4.0.2 + + goober@2.1.18(csstype@3.2.3): + dependencies: + csstype: 3.2.3 + google-auth-library@10.5.0: dependencies: base64-js: 1.5.1 @@ -20098,6 +22296,8 @@ snapshots: graceful-fs@4.2.11: {} + graphemer@1.4.0: {} + gray-matter@4.0.3: dependencies: js-yaml: 3.14.2 @@ -20105,6 +22305,8 @@ snapshots: section-matter: 1.0.0 strip-bom-string: 1.0.0 + grid-index@1.1.0: {} + gtoken@8.0.0: dependencies: gaxios: 7.1.3 @@ -20131,6 +22333,14 @@ snapshots: has-flag@4.0.0: {} + has-hover@1.0.1: + dependencies: + is-browser: 2.1.0 + + has-passive-events@1.0.0: + dependencies: + is-browser: 2.1.0 + has-property-descriptors@1.0.2: dependencies: es-define-property: 1.0.1 @@ -20613,6 +22823,10 @@ snapshots: immediate@3.3.0: {} + immer@10.2.0: {} + + immer@11.1.4: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -20649,6 +22863,8 @@ snapshots: ini@4.1.1: {} + ini@4.1.3: {} + ini@6.0.0: {} inline-style-parser@0.2.7: {} @@ -20697,6 +22913,8 @@ snapshots: dependencies: binary-extensions: 2.3.0 + is-browser@2.1.0: {} + is-buffer@2.0.5: {} is-callable@1.2.7: {} @@ -20719,6 +22937,10 @@ snapshots: is-extglob@2.1.1: {} + is-finite@1.1.0: {} + + is-firefox@1.0.3: {} + is-fullwidth-code-point@3.0.0: {} is-fullwidth-code-point@4.0.0: {} @@ -20744,6 +22966,8 @@ snapshots: is-interactive@2.0.0: {} + is-mobile@4.0.0: {} + is-network-error@1.3.0: {} is-npm@6.1.0: {} @@ -20756,6 +22980,8 @@ snapshots: is-path-inside@3.0.3: {} + is-plain-obj@1.1.0: {} + is-plain-obj@2.1.0: {} is-plain-obj@3.0.0: {} @@ -20780,6 +23006,10 @@ snapshots: is-stream@4.0.1: {} + is-string-blank@1.0.1: {} + + is-svg-path@1.0.2: {} + is-text-path@2.0.0: dependencies: text-extensions: 2.4.0 @@ -20808,8 +23038,12 @@ snapshots: isarray@2.0.5: {} + isbot@5.1.39: {} + isexe@2.0.0: {} + isexe@3.1.5: {} + isexe@4.0.0: {} isobject@3.0.1: {} @@ -20870,7 +23104,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 25.2.3 + '@types/node': 24.6.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -20970,6 +23204,8 @@ snapshots: json-stringify-nice@1.1.4: {} + json-stringify-pretty-compact@4.0.0: {} + json5@2.2.3: {} jsonata@2.1.0: @@ -21007,6 +23243,10 @@ snapshots: dependencies: commander: 8.3.0 + kdbush@3.0.0: {} + + kdbush@4.0.2: {} + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -21241,6 +23481,10 @@ snapshots: lru-cache@7.18.3: {} + lucide-react@0.546.0(react@19.2.0): + dependencies: + react: 19.2.0 + lucide-react@0.554.0(react@19.2.0): dependencies: react: 19.2.0 @@ -21285,6 +23529,64 @@ snapshots: - supports-color optional: true + map-limit@0.0.1: + dependencies: + once: 1.3.3 + + mapbox-gl@1.13.3: + dependencies: + '@mapbox/geojson-rewind': 0.5.2 + '@mapbox/geojson-types': 1.0.2 + '@mapbox/jsonlint-lines-primitives': 2.0.2 + '@mapbox/mapbox-gl-supported': 1.5.0(mapbox-gl@1.13.3) + '@mapbox/point-geometry': 0.1.0 + '@mapbox/tiny-sdf': 1.2.5 + '@mapbox/unitbezier': 0.0.0 + '@mapbox/vector-tile': 1.3.1 + '@mapbox/whoots-js': 3.1.0 + csscolorparser: 1.0.3 + earcut: 2.2.4 + geojson-vt: 3.2.1 + gl-matrix: 3.4.4 + grid-index: 1.1.0 + murmurhash-js: 1.0.0 + pbf: 3.3.0 + potpack: 1.0.2 + quickselect: 2.0.0 + rw: 1.3.3 + supercluster: 7.1.5 + tinyqueue: 2.0.3 + vt-pbf: 3.1.3 + + maplibre-gl@4.7.1: + dependencies: + '@mapbox/geojson-rewind': 0.5.2 + '@mapbox/jsonlint-lines-primitives': 2.0.2 + '@mapbox/point-geometry': 0.1.0 + '@mapbox/tiny-sdf': 2.1.0 + '@mapbox/unitbezier': 0.0.1 + '@mapbox/vector-tile': 1.3.1 + '@mapbox/whoots-js': 3.1.0 + '@maplibre/maplibre-gl-style-spec': 20.4.0 + '@types/geojson': 7946.0.16 + '@types/geojson-vt': 3.2.5 + '@types/mapbox__point-geometry': 0.1.4 + '@types/mapbox__vector-tile': 1.3.4 + '@types/pbf': 3.0.5 + '@types/supercluster': 7.1.3 + earcut: 3.0.2 + geojson-vt: 4.0.2 + gl-matrix: 3.4.4 + global-prefix: 4.0.0 + kdbush: 4.0.2 + murmurhash-js: 1.0.0 + pbf: 3.3.0 + potpack: 2.1.0 + quickselect: 3.0.0 + supercluster: 8.0.1 + tinyqueue: 3.0.0 + vt-pbf: 3.1.3 + mark.js@8.11.1: {} markdown-extensions@2.0.0: {} @@ -21310,6 +23612,8 @@ snapshots: math-intrinsics@1.1.0: {} + math-log2@1.0.1: {} + mdast-util-directive@3.1.0: dependencies: '@types/mdast': 4.0.4 @@ -21971,6 +24275,20 @@ snapshots: moment@2.30.1: {} + mouse-change@1.4.0: + dependencies: + mouse-event: 1.0.5 + + mouse-event-offset@3.0.2: {} + + mouse-event@1.0.5: {} + + mouse-wheel@1.2.0: + dependencies: + right-now: 1.0.0 + signum: 1.0.0 + to-px: 1.0.1 + mri@1.2.0: {} mrmime@2.0.1: {} @@ -21984,6 +24302,8 @@ snapshots: dns-packet: 5.6.1 thunky: 1.1.0 + murmurhash-js@1.0.0: {} + mute-stream@2.0.0: {} nanoid@3.3.11: {} @@ -21991,8 +24311,18 @@ snapshots: napi-build-utils@2.0.0: optional: true + native-promise-only@0.8.1: {} + natural-compare@1.4.0: {} + needle@2.9.1: + dependencies: + debug: 3.2.7 + iconv-lite: 0.4.24 + sax: 1.4.3 + transitivePeerDependencies: + - supports-color + negotiator@0.6.3: {} negotiator@0.6.4: {} @@ -22013,6 +24343,8 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + next-tick@1.1.0: {} + no-case@3.0.4: dependencies: lower-case: 2.0.2 @@ -22089,6 +24421,14 @@ snapshots: normalize-path@3.0.0: {} + normalize-range@0.1.2: {} + + normalize-svg-path@0.1.0: {} + + normalize-svg-path@1.1.0: + dependencies: + svg-arc-to-cubic-bezier: 3.2.0 + normalize-url@8.1.0: {} normalize-url@8.1.1: {} @@ -22135,6 +24475,10 @@ snapshots: schema-utils: 3.3.0 webpack: 5.103.0 + number-is-integer@1.0.1: + dependencies: + is-finite: 1.1.0 + nypm@0.6.2: dependencies: citty: 0.1.6 @@ -22175,10 +24519,13 @@ snapshots: on-headers@1.1.0: {} + once@1.3.3: + dependencies: + wrappy: 1.0.2 + once@1.4.0: dependencies: wrappy: 1.0.2 - optional: true onetime@5.1.2: dependencies: @@ -22192,6 +24539,14 @@ snapshots: dependencies: mimic-function: 5.0.1 + oniguruma-parser@0.12.2: {} + + oniguruma-to-es@4.3.6: + dependencies: + oniguruma-parser: 0.12.2 + regex: 6.1.0 + regex-recursion: 6.0.2 + open@10.2.0: dependencies: default-browser: 5.4.0 @@ -22339,6 +24694,8 @@ snapshots: dependencies: callsites: 3.1.0 + parenthesis@3.1.8: {} + parse-conflict-json@5.0.1: dependencies: json-parse-even-better-errors: 5.0.0 @@ -22368,6 +24725,14 @@ snapshots: dependencies: protocols: 2.0.2 + parse-rect@1.2.0: + dependencies: + pick-by-alias: 1.2.0 + + parse-svg-path@0.1.2: {} + + parse-unit@1.0.1: {} + parse-url@9.2.0: dependencies: '@types/parse-path': 7.1.0 @@ -22433,8 +24798,15 @@ snapshots: pathval@2.0.1: {} + pbf@3.3.0: + dependencies: + ieee754: 1.2.1 + resolve-protobuf-schema: 2.1.0 + perfect-debounce@2.0.0: {} + performance-now@2.1.0: {} + pg-cloudflare@1.3.0: optional: true @@ -22470,6 +24842,8 @@ snapshots: dependencies: split2: 4.2.0 + pick-by-alias@1.2.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -22502,6 +24876,64 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + plotly.js@3.5.0(mapbox-gl@1.13.3): + dependencies: + '@plotly/d3': 3.8.2 + '@plotly/d3-sankey': 0.7.2 + '@plotly/d3-sankey-circular': 0.33.1 + '@plotly/mapbox-gl': 1.13.4(mapbox-gl@1.13.3) + '@plotly/regl': 2.1.2 + '@turf/area': 7.3.5 + '@turf/bbox': 7.3.5 + '@turf/centroid': 7.3.5 + base64-arraybuffer: 1.0.2 + canvas-fit: 1.5.0 + color-alpha: 1.0.4 + color-normalize: 1.5.0 + color-parse: 2.0.0 + color-rgba: 3.0.0 + country-regex: 1.1.0 + d3-force: 1.2.1 + d3-format: 1.4.5 + d3-geo: 1.12.1 + d3-geo-projection: 2.9.0 + d3-hierarchy: 1.1.9 + d3-interpolate: 3.0.1 + d3-time: 1.1.0 + d3-time-format: 2.3.0 + fast-isnumeric: 1.1.4 + gl-mat4: 1.2.0 + gl-text: 1.4.0 + has-hover: 1.0.1 + has-passive-events: 1.0.0 + is-mobile: 4.0.0 + maplibre-gl: 4.7.1 + mouse-change: 1.4.0 + mouse-event-offset: 3.0.2 + mouse-wheel: 1.2.0 + native-promise-only: 0.8.1 + parse-svg-path: 0.1.2 + point-in-polygon: 1.1.0 + polybooljs: 1.2.2 + probe-image-size: 7.2.3 + regl-error2d: 2.0.12 + regl-line2d: 3.1.3 + regl-scatter2d: 3.4.0 + regl-splom: 1.0.14 + strongly-connected-components: 1.0.1 + superscript-text: 1.0.0 + svg-path-sdf: 1.1.3 + tinycolor2: 1.6.0 + to-px: 1.0.1 + topojson-client: 3.1.0 + webgl-context: 2.2.0 + world-calendars: 1.0.4 + transitivePeerDependencies: + - mapbox-gl + - supports-color + + point-in-polygon@1.1.0: {} + points-on-curve@0.2.0: {} points-on-path@0.2.1: @@ -22509,6 +24941,8 @@ snapshots: path-data-parser: 0.1.0 points-on-curve: 0.2.0 + polybooljs@1.2.2: {} + possible-typed-array-names@1.1.0: {} postcss-attribute-case-insensitive@7.0.1(postcss@8.5.6): @@ -22960,6 +25394,10 @@ snapshots: dependencies: xtend: 4.0.2 + potpack@1.0.2: {} + + potpack@2.1.0: {} + prebuild-install@7.1.3: dependencies: detect-libc: 2.1.2 @@ -23001,6 +25439,14 @@ snapshots: prismjs@1.30.0: {} + probe-image-size@7.2.3: + dependencies: + lodash.merge: 4.6.2 + needle: 2.9.1 + stream-parser: 0.3.1 + transitivePeerDependencies: + - supports-color + proc-log@6.1.0: {} process-nextick-args@2.0.1: {} @@ -23046,6 +25492,8 @@ snapshots: '@types/node': 25.2.3 long: 5.3.2 + protocol-buffers-schema@3.6.1: {} + protocols@2.0.2: {} proxy-addr@2.0.7: @@ -23104,6 +25552,14 @@ snapshots: quick-lru@5.1.1: {} + quickselect@2.0.0: {} + + quickselect@3.0.0: {} + + raf@3.4.1: + dependencies: + performance-now: 2.1.0 + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -23177,6 +25633,21 @@ snapshots: react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.0)' webpack: 5.103.0 + react-plotly.js@2.6.0(plotly.js@3.5.0(mapbox-gl@1.13.3))(react@19.2.0): + dependencies: + plotly.js: 3.5.0(mapbox-gl@1.13.3) + prop-types: 15.8.1 + react: 19.2.0 + + react-redux@9.2.0(@types/react@19.2.2)(react@19.2.0)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.2.0 + use-sync-external-store: 1.6.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + redux: 5.0.1 + react-refresh@0.17.0: {} react-refresh@0.18.0: {} @@ -23269,6 +25740,13 @@ snapshots: json-parse-even-better-errors: 5.0.0 npm-normalize-package-bin: 5.0.0 + readable-stream@1.0.34: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 0.0.1 + string_decoder: 0.10.31 + readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -23291,6 +25769,14 @@ snapshots: readdirp@5.0.0: {} + recast@0.23.11: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + recharts-scale@0.4.5: dependencies: decimal.js-light: 2.5.1 @@ -23308,6 +25794,26 @@ snapshots: tiny-invariant: 1.3.3 victory-vendor: 36.9.2 + recharts@3.4.1(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react-is@18.3.1)(react@19.2.0)(redux@5.0.1): + dependencies: + '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.2)(react@19.2.0)(redux@5.0.1))(react@19.2.0) + clsx: 2.1.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.46.0 + eventemitter3: 5.0.1 + immer: 10.2.0 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-is: 18.3.1 + react-redux: 9.2.0(@types/react@19.2.2)(react@19.2.0)(redux@5.0.1) + reselect: 5.1.1 + tiny-invariant: 1.3.3 + use-sync-external-store: 1.6.0(react@19.2.0) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux + recma-build-jsx@1.0.0: dependencies: '@types/estree': 1.0.8 @@ -23337,6 +25843,12 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + reflect-metadata@0.2.2: {} regenerate-unicode-properties@10.2.2: @@ -23345,6 +25857,16 @@ snapshots: regenerate@1.4.2: {} + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + regexpu-core@6.4.0: dependencies: regenerate: 1.4.2 @@ -23368,6 +25890,56 @@ snapshots: dependencies: jsesc: 3.1.0 + regl-error2d@2.0.12: + dependencies: + array-bounds: 1.0.1 + color-normalize: 1.5.0 + flatten-vertex-data: 1.0.2 + object-assign: 4.1.1 + pick-by-alias: 1.2.0 + to-float32: 1.1.0 + update-diff: 1.1.0 + + regl-line2d@3.1.3: + dependencies: + array-bounds: 1.0.1 + array-find-index: 1.0.2 + array-normalize: 1.1.4 + color-normalize: 1.5.0 + earcut: 2.2.4 + es6-weak-map: 2.0.3 + flatten-vertex-data: 1.0.2 + object-assign: 4.1.1 + parse-rect: 1.2.0 + pick-by-alias: 1.2.0 + to-float32: 1.1.0 + + regl-scatter2d@3.4.0: + dependencies: + '@plotly/point-cluster': 3.1.9 + array-bounds: 1.0.1 + color-id: 1.1.0 + color-normalize: 1.5.0 + flatten-vertex-data: 1.0.2 + glslify: 7.1.1 + parse-rect: 1.2.0 + pick-by-alias: 1.2.0 + to-float32: 1.1.0 + update-diff: 1.1.0 + + regl-splom@1.0.14: + dependencies: + array-bounds: 1.0.1 + array-range: 1.0.1 + color-alpha: 1.0.4 + flatten-vertex-data: 1.0.2 + parse-rect: 1.2.0 + pick-by-alias: 1.2.0 + raf: 3.4.1 + regl-scatter2d: 3.4.0 + + regl@2.1.1: {} + rehype-minify-whitespace@6.0.2: dependencies: '@types/hast': 3.0.4 @@ -23530,6 +26102,8 @@ snapshots: requires-port@1.0.0: {} + reselect@5.1.1: {} + resolve-alpn@1.2.1: {} resolve-from@4.0.0: {} @@ -23540,6 +26114,12 @@ snapshots: resolve-pkg-maps@1.0.0: {} + resolve-protobuf-schema@2.1.0: + dependencies: + protocol-buffers-schema: 3.6.1 + + resolve@0.6.3: {} + resolve@1.22.10: dependencies: is-core-module: 2.16.1 @@ -23567,6 +26147,8 @@ snapshots: rfdc@1.4.1: {} + right-now@1.0.0: {} + rimraf@5.0.10: dependencies: glob: 10.5.0 @@ -23608,6 +26190,24 @@ snapshots: tsx: 4.20.6 yaml: 2.8.2 + rolldown-vite@7.1.14(@types/node@24.6.0)(esbuild@0.25.10)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2): + dependencies: + '@oxc-project/runtime': 0.92.0 + fdir: 6.5.0(picomatch@4.0.3) + lightningcss: 1.30.2 + picomatch: 4.0.3 + postcss: 8.5.6 + rolldown: 1.0.0-beta.41 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.6.0 + esbuild: 0.25.10 + fsevents: 2.3.3 + jiti: 2.6.1 + terser: 5.44.1 + tsx: 4.20.6 + yaml: 2.8.2 + rolldown-vite@7.1.14(@types/node@25.2.3)(esbuild@0.25.10)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2): dependencies: '@oxc-project/runtime': 0.92.0 @@ -23772,9 +26372,9 @@ snapshots: schema-utils@4.3.3: dependencies: '@types/json-schema': 7.0.15 - ajv: 8.17.1 - ajv-formats: 2.1.1(ajv@8.17.1) - ajv-keywords: 5.1.0(ajv@8.17.1) + ajv: 8.18.0 + ajv-formats: 2.1.1(ajv@8.18.0) + ajv-keywords: 5.1.0(ajv@8.18.0) search-insights@2.17.3: {} @@ -23865,6 +26465,12 @@ snapshots: dependencies: randombytes: 2.1.0 + seroval-plugins@1.5.2(seroval@1.5.2): + dependencies: + seroval: 1.5.2 + + seroval@1.5.2: {} + serve-handler@6.1.6: dependencies: bytes: 3.0.0 @@ -23919,6 +26525,8 @@ snapshots: dependencies: kind-of: 6.0.3 + shallow-copy@0.0.1: {} + shallowequal@1.1.0: {} shebang-command@2.0.0: @@ -23929,6 +26537,17 @@ snapshots: shell-quote@1.8.3: {} + shiki@3.15.0: + dependencies: + '@shikijs/core': 3.15.0 + '@shikijs/engine-javascript': 3.15.0 + '@shikijs/engine-oniguruma': 3.15.0 + '@shikijs/langs': 3.15.0 + '@shikijs/themes': 3.15.0 + '@shikijs/types': 3.15.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -23963,6 +26582,8 @@ snapshots: signal-exit@4.1.0: {} + signum@1.0.0: {} + simple-concat@1.0.1: optional: true @@ -24042,6 +26663,12 @@ snapshots: ip-address: 10.1.0 smart-buffer: 4.2.0 + solid-js@1.9.12: + dependencies: + csstype: 3.2.3 + seroval: 1.5.2 + seroval-plugins: 1.5.2(seroval@1.5.2) + sonner@2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: react: 19.2.0 @@ -24116,8 +26743,14 @@ snapshots: dependencies: minipass: 7.1.2 + stack-trace@0.0.9: {} + stackback@0.0.2: {} + static-eval@2.1.1: + dependencies: + escodegen: 2.1.0 + statuses@1.5.0: {} statuses@2.0.1: {} @@ -24128,8 +26761,20 @@ snapshots: stdin-discarder@0.2.2: {} + stream-parser@0.3.1: + dependencies: + debug: 2.6.9 + transitivePeerDependencies: + - supports-color + + stream-shift@1.0.3: {} + string-argv@0.3.2: {} + string-split-by@1.0.0: + dependencies: + parenthesis: 3.1.8 + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -24153,6 +26798,8 @@ snapshots: get-east-asian-width: 1.4.0 strip-ansi: 7.1.2 + string_decoder@0.10.31: {} + string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 @@ -24196,6 +26843,8 @@ snapshots: dependencies: js-tokens: 9.0.1 + strongly-connected-components@1.0.1: {} + style-to-js@1.1.21: dependencies: style-to-object: 1.0.14 @@ -24212,6 +26861,16 @@ snapshots: stylis@4.3.6: {} + supercluster@7.1.5: + dependencies: + kdbush: 3.0.0 + + supercluster@8.0.1: + dependencies: + kdbush: 4.0.2 + + superscript-text@1.0.0: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -24222,8 +26881,25 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svg-arc-to-cubic-bezier@3.2.0: {} + svg-parser@2.0.4: {} + svg-path-bounds@1.0.2: + dependencies: + abs-svg-path: 0.1.1 + is-svg-path: 1.0.2 + normalize-svg-path: 1.1.0 + parse-svg-path: 0.1.2 + + svg-path-sdf@1.1.3: + dependencies: + bitmap-sdf: 1.0.4 + draw-svg-path: 1.0.0 + is-svg-path: 1.0.2 + parse-svg-path: 0.1.2 + svg-path-bounds: 1.0.2 + svgo@3.3.2: dependencies: '@trysound/sax': 0.2.0 @@ -24255,6 +26931,8 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + tailwind-merge@3.3.1: {} + tailwind-merge@3.4.0: {} tailwindcss-animate@1.0.7(tailwindcss@4.1.17): @@ -24292,6 +26970,18 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 + terser-webpack-plugin@5.3.16(esbuild@0.25.10)(webpack@5.103.0(esbuild@0.25.10)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + serialize-javascript: 6.0.2 + terser: 5.44.1 + webpack: 5.103.0(esbuild@0.25.10) + optionalDependencies: + esbuild: 0.25.10 + optional: true + terser-webpack-plugin@5.3.16(webpack@5.103.0): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -24322,6 +27012,16 @@ snapshots: throttleit@2.1.0: {} + through2@0.6.5: + dependencies: + readable-stream: 1.0.34 + xtend: 4.0.2 + + through2@2.0.5: + dependencies: + readable-stream: 2.3.8 + xtend: 4.0.2 + through@2.3.8: {} thunky@1.1.0: {} @@ -24332,6 +27032,8 @@ snapshots: tinybench@2.9.0: {} + tinycolor2@1.6.0: {} + tinyexec@0.3.2: {} tinyexec@1.0.2: {} @@ -24343,6 +27045,10 @@ snapshots: tinypool@1.1.1: {} + tinyqueue@2.0.3: {} + + tinyqueue@3.0.0: {} + tinyrainbow@2.0.0: {} tinyspy@4.0.4: {} @@ -24359,6 +27065,12 @@ snapshots: safe-buffer: 5.2.1 typed-array-buffer: 1.0.3 + to-float32@1.1.0: {} + + to-px@1.0.1: + dependencies: + parse-unit: 1.0.1 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -24370,6 +27082,10 @@ snapshots: toidentifier@1.0.1: {} + topojson-client@3.1.0: + dependencies: + commander: 2.20.3 + toposort-class@1.0.1: {} totalist@3.0.1: {} @@ -24507,12 +27223,19 @@ snapshots: mime-types: 3.0.2 optional: true + type@2.7.3: {} + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 es-errors: 1.3.0 is-typed-array: 1.1.15 + typedarray-pool@1.2.0: + dependencies: + bit-twiddle: 1.0.2 + dup: 1.0.0 + typedarray-to-buffer@3.1.5: dependencies: is-typedarray: 1.0.0 @@ -24559,6 +27282,17 @@ snapshots: - babel-plugin-macros - supports-color + typescript-eslint@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.36.0(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + typescript-eslint@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) @@ -24592,6 +27326,8 @@ snapshots: undici-types@6.21.0: {} + undici-types@7.13.0: {} + undici-types@7.14.0: {} undici-types@7.16.0: {} @@ -24698,15 +27434,18 @@ snapshots: unpipe@1.0.0: {} - unrun@0.2.28: + unplugin@2.3.11: dependencies: - rolldown: 1.0.0-rc.5 + '@jridgewell/remapping': 2.3.5 + acorn: 8.15.0 + picomatch: 4.0.3 + webpack-virtual-modules: 0.6.2 + + unquote@1.1.1: {} - update-browserslist-db@1.1.4(browserslist@4.28.0): + unrun@0.2.28: dependencies: - browserslist: 4.28.0 - escalade: 3.2.0 - picocolors: 1.1.1 + rolldown: 1.0.0-rc.5 update-browserslist-db@1.2.2(browserslist@4.28.1): dependencies: @@ -24714,6 +27453,8 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-diff@1.1.0: {} + update-notifier@6.0.2: dependencies: boxen: 7.1.1 @@ -24849,6 +27590,23 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 + victory-vendor@37.3.6: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + vite-node@3.2.4(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2): dependencies: cac: 6.7.14 @@ -24898,6 +27656,23 @@ snapshots: tsx: 4.20.6 yaml: 2.8.2 + vite@7.2.4(@types/node@24.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2): + dependencies: + esbuild: 0.25.10 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.4 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.6.0 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + terser: 5.44.1 + tsx: 4.20.6 + yaml: 2.8.2 + vite@7.2.4(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2): dependencies: esbuild: 0.25.10 @@ -24975,6 +27750,12 @@ snapshots: vscode-uri@3.0.8: {} + vt-pbf@3.1.3: + dependencies: + '@mapbox/point-geometry': 0.1.0 + '@mapbox/vector-tile': 1.3.1 + pbf: 3.3.0 + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 @@ -24990,12 +27771,18 @@ snapshots: dependencies: minimalistic-assert: 1.0.1 + weak-map@1.0.8: {} + web-namespaces@1.1.4: {} web-namespaces@2.0.1: {} web-streams-polyfill@3.3.3: {} + webgl-context@2.2.0: + dependencies: + get-canvas-context: 1.0.2 + webidl-conversions@3.0.1: {} webidl-conversions@8.0.0: {} @@ -25081,6 +27868,8 @@ snapshots: webpack-sources@3.3.3: {} + webpack-virtual-modules@0.6.2: {} + webpack@5.103.0: dependencies: '@types/eslint-scope': 3.7.7 @@ -25113,6 +27902,39 @@ snapshots: - esbuild - uglify-js + webpack@5.103.0(esbuild@0.25.10): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.15.0 + acorn-import-phases: 1.0.4(acorn@8.15.0) + browserslist: 4.28.1 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.18.3 + es-module-lexer: 1.7.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.1 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.0 + terser-webpack-plugin: 5.3.16(esbuild@0.25.10)(webpack@5.103.0(esbuild@0.25.10)) + watchpack: 2.4.4 + webpack-sources: 3.3.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + optional: true + webpackbar@6.0.1(webpack@5.103.0): dependencies: ansi-escapes: 4.3.2 @@ -25163,6 +27985,10 @@ snapshots: dependencies: isexe: 2.0.0 + which@4.0.0: + dependencies: + isexe: 3.1.5 + which@6.0.1: dependencies: isexe: 4.0.0 @@ -25198,6 +28024,10 @@ snapshots: wordwrapjs@5.1.1: {} + world-calendars@1.0.4: + dependencies: + object-assign: 4.1.1 + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -25222,8 +28052,7 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.2 - wrappy@1.0.2: - optional: true + wrappy@1.0.2: {} write-file-atomic@3.0.3: dependencies: @@ -25258,6 +28087,8 @@ snapshots: xmlchars@2.2.0: {} + xtend@2.2.0: {} + xtend@4.0.2: {} y18n@5.0.8: {} @@ -25306,6 +28137,8 @@ snapshots: dependencies: zod: 4.1.13 + zod@3.25.76: {} + zod@4.1.13: {} zod@4.3.6: {} From f2697579287b22b81439c99e700bcb67169ce623 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Thu, 30 Apr 2026 09:22:43 +0200 Subject: [PATCH 08/34] fix(appkit-ui): surface server error message in useMetricView dev mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the metric route fails at the connection layer (server-thrown 500, stream close before SSE error event, etc.) onError previously dropped err.message and replaced it with the generic "Unable to load data, please try again". This matched the production UX intent but was hostile to development — schema-not-found, auth-failed, and other diagnosable failures were invisible to the developer. In dev (import.meta.env.DEV), surface err.message verbatim. In prod, keep the generic line. Full error stays in console.error in both modes. Mirrors the same pattern useAnalyticsQuery uses (and which has the same bug at use-analytics-query.ts:177); not fixing the analytics hook in this commit because the analytics surface is unchanged scope from this loop. Tests: +2 cases covering dev-mode passthrough and prod-mode generic fallback (vi.stubEnv("DEV", "")). 20/20 pass. Co-authored-by: Isaac Signed-off-by: Atila Fassina --- .../shared/appkit-types/metrics.metadata.json | 98 +++++++++---------- .../hooks/__tests__/use-metric-view.test.ts | 46 +++++++++ .../src/react/hooks/use-metric-view.ts | 7 ++ 3 files changed, 98 insertions(+), 53 deletions(-) diff --git a/apps/dev-playground/shared/appkit-types/metrics.metadata.json b/apps/dev-playground/shared/appkit-types/metrics.metadata.json index 87a519b9a..dfa1586ad 100644 --- a/apps/dev-playground/shared/appkit-types/metrics.metadata.json +++ b/apps/dev-playground/shared/appkit-types/metrics.metadata.json @@ -1,78 +1,70 @@ { - "revenue": { - "source": "appkit_demo.public.revenue_metrics", - "lane": "sp", + "customer_metrics": { + "source": "appkit_demo.public.customer_metrics", + "lane": "obo", "measures": { - "arr": { - "type": "DECIMAL(38,2)", - "display_name": "Annual Recurring Revenue", - "format": "$#,##0.00", - "description": "Annualized contract value across all active subscriptions" - }, - "mrr": { - "type": "DECIMAL(38,2)", - "display_name": "Monthly Recurring Revenue", - "format": "$#,##0.00" + "active_accounts": { + "type": "bigint", + "display_name": "Active Accounts" }, - "new_arr": { - "type": "DECIMAL(38,2)", - "display_name": "New ARR", - "format": "$#,##0.00" + "churn_rate": { + "type": "decimal", + "display_name": "Churn Rate" }, - "churned_arr": { - "type": "DECIMAL(38,2)", - "display_name": "Churned ARR", - "format": "$#,##0.00" + "avg_ltv": { + "type": "double", + "display_name": "Average LTV" } }, "dimensions": { - "region": { - "type": "STRING", - "display_name": "Region" - }, "segment": { - "type": "STRING", + "type": "string", "display_name": "Customer Segment" }, - "created_at": { - "type": "TIMESTAMP", - "display_name": "Subscription Start", - "time_grain": ["day", "week", "month", "quarter", "year"] + "region": { + "type": "string", + "display_name": "Region" + }, + "csm_email": { + "type": "string", + "display_name": "CSM Email" } } }, - "customer_metrics": { - "source": "appkit_demo.public.customer_metrics", - "lane": "obo", + "revenue": { + "source": "appkit_demo.public.revenue_metrics", + "lane": "sp", "measures": { - "active_accounts": { - "type": "BIGINT", - "display_name": "Active Accounts", - "format": "#,##0" + "mrr": { + "type": "double", + "display_name": "Monthly Recurring Revenue" }, - "churn_rate": { - "type": "DECIMAL(10,4)", - "display_name": "Churn Rate", - "format": "0.0%" + "arr": { + "type": "double", + "display_name": "Annual Recurring Revenue", + "description": "Annualized contract value across all active subscriptions" }, - "avg_ltv": { - "type": "DECIMAL(38,2)", - "display_name": "Average LTV", - "format": "$#,##0.00" + "new_arr": { + "type": "double", + "display_name": "New ARR" + }, + "churned_arr": { + "type": "double", + "display_name": "Churned ARR" } }, "dimensions": { - "segment": { - "type": "STRING", - "display_name": "Customer Segment" - }, "region": { - "type": "STRING", + "type": "string", "display_name": "Region" }, - "csm_email": { - "type": "STRING", - "display_name": "CSM Email" + "segment": { + "type": "string", + "display_name": "Customer Segment" + }, + "created_at": { + "type": "timestamp_ltz", + "display_name": "Subscription Start" } } } diff --git a/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts index 1cc29bbf4..4d0d569d3 100644 --- a/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts +++ b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts @@ -287,6 +287,52 @@ describe("useMetricView", () => { expect(result.current.error).toMatch(/Network error/); }); + test("in dev, surfaces the actual error message via onError", async () => { + // Vitest sets import.meta.env.DEV = true by default, mirroring Vite dev. + const { result } = renderHook(() => + useMetricView("revenue", { measures: ["arr"] }), + ); + + act(() => { + capturedCallbacks.onError?.( + new Error( + "[TABLE_OR_VIEW_NOT_FOUND] appkit_demo.public.revenue_metrics", + ), + ); + }); + + await waitFor(() => { + expect(result.current.loading).toBe(false); + }); + expect(result.current.error).toBe( + "[TABLE_OR_VIEW_NOT_FOUND] appkit_demo.public.revenue_metrics", + ); + }); + + test("in prod, falls back to the generic message via onError", async () => { + vi.stubEnv("DEV", ""); + try { + const { result } = renderHook(() => + useMetricView("revenue", { measures: ["arr"] }), + ); + + act(() => { + capturedCallbacks.onError?.( + new Error("[TABLE_OR_VIEW_NOT_FOUND] schema.foo.bar"), + ); + }); + + await waitFor(() => { + expect(result.current.loading).toBe(false); + }); + expect(result.current.error).toBe( + "Unable to load data, please try again", + ); + } finally { + vi.unstubAllEnvs(); + } + }); + test("does NOT auto-start when autoStart=false", () => { renderHook(() => useMetricView("revenue", { measures: ["arr"] }, { autoStart: false }), diff --git a/packages/appkit-ui/src/react/hooks/use-metric-view.ts b/packages/appkit-ui/src/react/hooks/use-metric-view.ts index b4216a417..f63f22522 100644 --- a/packages/appkit-ui/src/react/hooks/use-metric-view.ts +++ b/packages/appkit-ui/src/react/hooks/use-metric-view.ts @@ -195,6 +195,13 @@ export function useMetricView< userMessage = "Request timed out, please try again"; } else if (err.message.includes("Failed to fetch")) { userMessage = "Network error. Please check your connection."; + } else if (import.meta.env.DEV && err.message) { + // In dev, surface the actual error so developers can diagnose + // schema-not-found, auth-failed, and other server-thrown + // failures that didn't make it into an SSE error event. + // Production keeps the generic message — the full error is + // still in the console.error below for ops. + userMessage = err.message; } console.error("[useMetricView] Error", { metricKey, From 7bfc8e42c72fb688197356f12fdb6297a6d775f9 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Thu, 30 Apr 2026 09:27:02 +0200 Subject: [PATCH 09/34] fix(playground): standardize on metric.d.ts (singular) as the type-gen artifact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 7 hand-authored metrics.d.ts (plural) as a fallback type-registry augmentation while the dev workspace lacked the underlying UC metric views. Phase 1's type-gen always emitted metric.d.ts (singular). The naming diverged. Now that the seed creates the actual views, `npx appkit metric sync` emits the canonical singular file. Drop the plural hand-authored stand-in; the auto-generated singular file is the source of truth. Also fixes vi.stubEnv("DEV", "") → vi.stubEnv("DEV", false) — vitest's typed signature requires a boolean for the DEV key (the empty-string worked at runtime but failed typecheck). Updates one stale `metrics.d.ts` reference in the dev-playground demo route's UI text. Co-authored-by: Isaac Signed-off-by: Atila Fassina --- .../client/src/routes/metrics.route.tsx | 2 +- .../shared/appkit-types/metric.d.ts | 122 ++++++++++++++++ .../shared/appkit-types/metrics.d.ts | 135 ------------------ .../hooks/__tests__/use-metric-view.test.ts | 2 +- 4 files changed, 124 insertions(+), 137 deletions(-) create mode 100644 apps/dev-playground/shared/appkit-types/metric.d.ts delete mode 100644 apps/dev-playground/shared/appkit-types/metrics.d.ts diff --git a/apps/dev-playground/client/src/routes/metrics.route.tsx b/apps/dev-playground/client/src/routes/metrics.route.tsx index 73ea56862..b703ecc62 100644 --- a/apps/dev-playground/client/src/routes/metrics.route.tsx +++ b/apps/dev-playground/client/src/routes/metrics.route.tsx @@ -336,7 +336,7 @@ function MetricsRoute() {
  • npx appkit metric sync regenerates a typed{" "} - metrics.d.ts (augmenting{" "} + metric.d.ts (augmenting{" "} MetricRegistry) and a{" "} metrics.metadata.json bundle.
  • diff --git a/apps/dev-playground/shared/appkit-types/metric.d.ts b/apps/dev-playground/shared/appkit-types/metric.d.ts new file mode 100644 index 000000000..e9f401f08 --- /dev/null +++ b/apps/dev-playground/shared/appkit-types/metric.d.ts @@ -0,0 +1,122 @@ +// Auto-generated by AppKit - DO NOT EDIT +// Generated by 'npx @databricks/appkit generate-types' or Vite plugin during build +import "@databricks/appkit-ui/react"; +declare module "@databricks/appkit-ui/react" { + interface MetricRegistry { + "revenue": { + key: "revenue"; + source: "appkit_demo.public.revenue_metrics"; + lane: "sp"; + measures: { + /** @sqlType double */ + "mrr": number; + /** @sqlType double */ + "arr": number; + /** @sqlType double */ + "new_arr": number; + /** @sqlType double */ + "churned_arr": number; + }; + dimensions: { + /** @sqlType string */ + "region": string; + /** @sqlType string */ + "segment": string; + /** @sqlType timestamp_ltz */ + "created_at": string; + }; + measureKeys: "mrr" | "arr" | "new_arr" | "churned_arr"; + dimensionKeys: "region" | "segment" | "created_at"; + timeGrains: never; + metadata: { + measures: { + "mrr": { + type: "double"; + display_name: "Monthly Recurring Revenue"; + }; + "arr": { + type: "double"; + display_name: "Annual Recurring Revenue"; + description: "Annualized contract value across all active subscriptions"; + }; + "new_arr": { + type: "double"; + display_name: "New ARR"; + }; + "churned_arr": { + type: "double"; + display_name: "Churned ARR"; + }; + }; + dimensions: { + "region": { + type: "string"; + display_name: "Region"; + }; + "segment": { + type: "string"; + display_name: "Customer Segment"; + }; + "created_at": { + type: "timestamp_ltz"; + display_name: "Subscription Start"; + }; + }; + }; + }; + "customer_metrics": { + key: "customer_metrics"; + source: "appkit_demo.public.customer_metrics"; + lane: "obo"; + measures: { + /** @sqlType bigint */ + "active_accounts": number; + /** @sqlType decimal */ + "churn_rate": number; + /** @sqlType double */ + "avg_ltv": number; + }; + dimensions: { + /** @sqlType string */ + "segment": string; + /** @sqlType string */ + "region": string; + /** @sqlType string */ + "csm_email": string; + }; + measureKeys: "active_accounts" | "churn_rate" | "avg_ltv"; + dimensionKeys: "segment" | "region" | "csm_email"; + timeGrains: never; + metadata: { + measures: { + "active_accounts": { + type: "bigint"; + display_name: "Active Accounts"; + }; + "churn_rate": { + type: "decimal"; + display_name: "Churn Rate"; + }; + "avg_ltv": { + type: "double"; + display_name: "Average LTV"; + }; + }; + dimensions: { + "segment": { + type: "string"; + display_name: "Customer Segment"; + }; + "region": { + type: "string"; + display_name: "Region"; + }; + "csm_email": { + type: "string"; + display_name: "CSM Email"; + }; + }; + }; + }; + } +} diff --git a/apps/dev-playground/shared/appkit-types/metrics.d.ts b/apps/dev-playground/shared/appkit-types/metrics.d.ts deleted file mode 100644 index c9d41b398..000000000 --- a/apps/dev-playground/shared/appkit-types/metrics.d.ts +++ /dev/null @@ -1,135 +0,0 @@ -// Auto-generated by AppKit - DO NOT EDIT -// In a production app this file is regenerated by `npx @databricks/appkit metric sync` -// or the Vite type-generator plugin. Phase 7 ships this hand-authored copy so the -// dev-playground demo at /metrics compiles even when the dev workspace does not -// host the underlying UC metric views — the typed surface is what we want to -// showcase. -import "@databricks/appkit-ui/react"; - -declare module "@databricks/appkit-ui/react" { - interface MetricRegistry { - revenue: { - key: "revenue"; - source: "appkit_demo.public.revenue_metrics"; - lane: "sp"; - measures: { - /** @sqlType DECIMAL(38,2) */ - arr: number; - /** @sqlType DECIMAL(38,2) */ - mrr: number; - /** @sqlType DECIMAL(38,2) */ - new_arr: number; - /** @sqlType DECIMAL(38,2) */ - churned_arr: number; - }; - dimensions: { - /** @sqlType STRING */ - region: string; - /** @sqlType STRING */ - segment: string; - /** @sqlType TIMESTAMP @timeGrain day|week|month|quarter|year */ - created_at: string; - }; - measureKeys: "arr" | "mrr" | "new_arr" | "churned_arr"; - dimensionKeys: "region" | "segment" | "created_at"; - timeGrains: "day" | "week" | "month" | "quarter" | "year"; - metadata: { - measures: { - arr: { - type: "DECIMAL(38,2)"; - display_name: "Annual Recurring Revenue"; - format: "$#,##0.00"; - description: "Annualized contract value across all active subscriptions"; - }; - mrr: { - type: "DECIMAL(38,2)"; - display_name: "Monthly Recurring Revenue"; - format: "$#,##0.00"; - }; - new_arr: { - type: "DECIMAL(38,2)"; - display_name: "New ARR"; - format: "$#,##0.00"; - }; - churned_arr: { - type: "DECIMAL(38,2)"; - display_name: "Churned ARR"; - format: "$#,##0.00"; - }; - }; - dimensions: { - region: { - type: "STRING"; - display_name: "Region"; - }; - segment: { - type: "STRING"; - display_name: "Customer Segment"; - }; - created_at: { - type: "TIMESTAMP"; - display_name: "Subscription Start"; - time_grain: readonly ["day", "week", "month", "quarter", "year"]; - }; - }; - }; - }; - customer_metrics: { - key: "customer_metrics"; - source: "appkit_demo.public.customer_metrics"; - lane: "obo"; - measures: { - /** @sqlType BIGINT */ - active_accounts: number; - /** @sqlType DECIMAL(10,4) */ - churn_rate: number; - /** @sqlType DECIMAL(38,2) */ - avg_ltv: number; - }; - dimensions: { - /** @sqlType STRING */ - segment: string; - /** @sqlType STRING */ - region: string; - /** @sqlType STRING */ - csm_email: string; - }; - measureKeys: "active_accounts" | "churn_rate" | "avg_ltv"; - dimensionKeys: "segment" | "region" | "csm_email"; - timeGrains: never; - metadata: { - measures: { - active_accounts: { - type: "BIGINT"; - display_name: "Active Accounts"; - format: "#,##0"; - }; - churn_rate: { - type: "DECIMAL(10,4)"; - display_name: "Churn Rate"; - format: "0.0%"; - }; - avg_ltv: { - type: "DECIMAL(38,2)"; - display_name: "Average LTV"; - format: "$#,##0.00"; - }; - }; - dimensions: { - segment: { - type: "STRING"; - display_name: "Customer Segment"; - }; - region: { - type: "STRING"; - display_name: "Region"; - }; - csm_email: { - type: "STRING"; - display_name: "CSM Email"; - }; - }; - }; - }; - } -} diff --git a/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts index 4d0d569d3..a2cafa841 100644 --- a/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts +++ b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts @@ -310,7 +310,7 @@ describe("useMetricView", () => { }); test("in prod, falls back to the generic message via onError", async () => { - vi.stubEnv("DEV", ""); + vi.stubEnv("DEV", false); try { const { result } = renderHook(() => useMetricView("revenue", { measures: ["arr"] }), From 7b6cecf0e0aff63b79f938453d10fd97a20df16a Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Thu, 30 Apr 2026 09:36:29 +0200 Subject: [PATCH 10/34] fix(appkit): alias MEASURE() in metric SQL so result rows use plain keys Without an alias, Databricks returns measure columns named "measure(arr)" literal rather than "arr". Frontend consumers reading row. got undefined; the customer_metrics OBO panel in dev-playground rendered empty cells for active_accounts and churn_rate while the segment dimension (which doesn't go through MEASURE()) rendered fine. Add `AS ` to each MEASURE() emission. The measure name is already validated against MEASURE_NAME_PATTERN and the registry's known measure list before this point, so safe to interpolate. Tests: snapshot updates for buildMetricSql + 7 string assertions in the route-handler tests. 131/131 metric tests pass; full backpressure (build, docs, check:fix, typecheck, test, knip) green. Co-authored-by: Isaac Signed-off-by: Atila Fassina --- .../appkit/src/plugins/analytics/metric.ts | 7 +++++- .../plugins/analytics/tests/metric.test.ts | 22 +++++++++---------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/appkit/src/plugins/analytics/metric.ts b/packages/appkit/src/plugins/analytics/metric.ts index aae29827b..897e88ad5 100644 --- a/packages/appkit/src/plugins/analytics/metric.ts +++ b/packages/appkit/src/plugins/analytics/metric.ts @@ -762,9 +762,14 @@ export function buildMetricSql( // Deterministic order so cache keys collapse semantically equivalent calls. // Sort-before-hash composition is finalized in Phase 4; sorting the SELECT // list here is the same idea applied to the SQL itself. + // Alias each measure to its plain name so result rows have keys matching + // the registered measure (`{ arr: 1234 }`) rather than the SQL-function + // serialization Databricks returns by default (`{ "measure(arr)": 1234 }`). + // The measure name has already been validated against MEASURE_NAME_PATTERN + // and the registry's known measure list, so it's safe to interpolate. const measureClauses = [...request.measures] .sort() - .map((m) => `MEASURE(${m})`); + .map((m) => `MEASURE(${m}) AS ${m}`); const dimensionClauses = [...dimensions] .sort() diff --git a/packages/appkit/src/plugins/analytics/tests/metric.test.ts b/packages/appkit/src/plugins/analytics/tests/metric.test.ts index fb59b4c22..9e56fff34 100644 --- a/packages/appkit/src/plugins/analytics/tests/metric.test.ts +++ b/packages/appkit/src/plugins/analytics/tests/metric.test.ts @@ -267,7 +267,7 @@ describe("metric — pure helpers", () => { measures: ["arr"], }); expect(statement).toBe( - "SELECT MEASURE(arr) FROM appkit_demo.public.revenue_metrics", + "SELECT MEASURE(arr) AS arr FROM appkit_demo.public.revenue_metrics", ); // No filter present → no bind params. expect(parameters).toEqual({}); @@ -278,7 +278,7 @@ describe("metric — pure helpers", () => { measures: ["mrr", "arr"], }); expect(statement).toBe( - "SELECT MEASURE(arr), MEASURE(mrr) FROM appkit_demo.public.revenue_metrics", + "SELECT MEASURE(arr) AS arr, MEASURE(mrr) AS mrr FROM appkit_demo.public.revenue_metrics", ); }); @@ -288,7 +288,7 @@ describe("metric — pure helpers", () => { limit: 10, }); expect(statement).toBe( - "SELECT MEASURE(arr) FROM appkit_demo.public.revenue_metrics LIMIT 10", + "SELECT MEASURE(arr) AS arr FROM appkit_demo.public.revenue_metrics LIMIT 10", ); }); @@ -335,7 +335,7 @@ describe("metric — pure helpers", () => { measures: ["arr"], }); expect(statement).toMatchInlineSnapshot( - `"SELECT MEASURE(arr) FROM appkit_demo.public.revenue_metrics"`, + `"SELECT MEASURE(arr) AS arr FROM appkit_demo.public.revenue_metrics"`, ); }); @@ -345,7 +345,7 @@ describe("metric — pure helpers", () => { dimensions: ["region"], }); expect(statement).toMatchInlineSnapshot( - `"SELECT MEASURE(arr), region FROM appkit_demo.public.revenue_metrics GROUP BY ALL"`, + `"SELECT MEASURE(arr) AS arr, region FROM appkit_demo.public.revenue_metrics GROUP BY ALL"`, ); }); @@ -356,7 +356,7 @@ describe("metric — pure helpers", () => { timeGrain: "month", }); expect(statement).toMatchInlineSnapshot( - `"SELECT MEASURE(arr), date_trunc('month', created_at) AS created_at, region FROM appkit_demo.public.revenue_metrics GROUP BY ALL"`, + `"SELECT MEASURE(arr) AS arr, date_trunc('month', created_at) AS created_at, region FROM appkit_demo.public.revenue_metrics GROUP BY ALL"`, ); }); @@ -368,7 +368,7 @@ describe("metric — pure helpers", () => { limit: 50, }); expect(statement).toMatchInlineSnapshot( - `"SELECT MEASURE(arr), MEASURE(mrr), date_trunc('week', created_at) AS created_at FROM appkit_demo.public.revenue_metrics GROUP BY ALL LIMIT 50"`, + `"SELECT MEASURE(arr) AS arr, MEASURE(mrr) AS mrr, date_trunc('week', created_at) AS created_at FROM appkit_demo.public.revenue_metrics GROUP BY ALL LIMIT 50"`, ); }); @@ -444,7 +444,7 @@ describe("metric — pure helpers", () => { }); // region comes before segment alphabetically. expect(statement).toBe( - "SELECT MEASURE(arr), region, segment FROM appkit_demo.public.revenue_metrics GROUP BY ALL", + "SELECT MEASURE(arr) AS arr, region, segment FROM appkit_demo.public.revenue_metrics GROUP BY ALL", ); }); }); @@ -862,7 +862,7 @@ describe("AnalyticsPlugin — metric route handler", () => { expect.anything(), expect.objectContaining({ statement: - "SELECT MEASURE(arr) FROM appkit_demo.public.revenue_metrics", + "SELECT MEASURE(arr) AS arr FROM appkit_demo.public.revenue_metrics", warehouse_id: "test-warehouse-id", }), expect.any(AbortSignal), @@ -938,7 +938,7 @@ describe("AnalyticsPlugin — metric route handler", () => { expect.anything(), expect.objectContaining({ statement: - "SELECT MEASURE(arr), region FROM appkit_demo.public.revenue_metrics GROUP BY ALL", + "SELECT MEASURE(arr) AS arr, region FROM appkit_demo.public.revenue_metrics GROUP BY ALL", }), expect.any(AbortSignal), ); @@ -975,7 +975,7 @@ describe("AnalyticsPlugin — metric route handler", () => { expect.anything(), expect.objectContaining({ statement: - "SELECT MEASURE(arr), date_trunc('month', created_at) AS created_at FROM appkit_demo.public.revenue_metrics GROUP BY ALL", + "SELECT MEASURE(arr) AS arr, date_trunc('month', created_at) AS created_at FROM appkit_demo.public.revenue_metrics GROUP BY ALL", }), expect.any(AbortSignal), ); From fca252d94e935b103b6107a39e75c84b1971ed22 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Thu, 30 Apr 2026 10:06:54 +0200 Subject: [PATCH 11/34] fix(appkit): classify aborted operations as STREAM_ABORTED, not UPSTREAM_ERROR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SQL warehouse client (executeStatement and getStatement polling catches at client.ts:249, 389) unconditionally re-wrapped non-AppKitError errors as ExecutionError.statementFailed(error.message). When the error is the SDK's native AbortError, the wrap destroys name === "AbortError" and stamps statusCode 500 — the downstream stream-manager._categorizeError then matched the "statusCode" duck-type fallback and returned SSEErrorCode.UPSTREAM_ERROR, bypassing the "client cancellation is normal" short-circuit. Result: every React 19 StrictMode dev-mode double-mount (or any legitimate client cancel) logged at error level as if the warehouse failed. Defense-in-depth fix at both layers: 1. connectors/sql-warehouse/client.ts (both wrap sites): re-throw native AbortError as-is before the ExecutionError wrap. 2. stream/stream-manager.ts._categorizeError: message-substring check for "operation was aborted" / "the request was aborted" before falling through to the statusCode-based UPSTREAM_ERROR classification. Catches any future code path where AbortError identity gets laundered. Tests: new "error categorization" describe block in stream.test.ts covering native AbortError → STREAM_ABORTED, wrapped AbortError (whose name was overwritten) → STREAM_ABORTED via message match, real upstream error with statusCode → UPSTREAM_ERROR, timeout, ECONNREFUSED, opaque fallback. 6 new tests; full backpressure (build, docs, check:fix, typecheck, test, knip) green. Also: switch the dev-playground /metrics route's error fallbacks from amber-saturated alerts to neutral border-border + bg-muted/40 with the underlying error rendered in destructive-token monospace. Customer metrics table headers gain text-muted-foreground + uppercase tracking, numeric columns right-align with tabular-nums, rows gain a hover state. See investigations/appkit_2026-04-30_aborted-misclassified-as-upstream-error.md for the 5-remora investigation that produced this fix. Co-authored-by: Isaac Signed-off-by: Atila Fassina --- .../client/src/routes/metrics.route.tsx | 62 ++++++++----- .../src/connectors/sql-warehouse/client.ts | 14 +++ packages/appkit/src/stream/stream-manager.ts | 13 +++ .../appkit/src/stream/tests/stream.test.ts | 90 +++++++++++++++++++ 4 files changed, 156 insertions(+), 23 deletions(-) diff --git a/apps/dev-playground/client/src/routes/metrics.route.tsx b/apps/dev-playground/client/src/routes/metrics.route.tsx index b703ecc62..2455ae340 100644 --- a/apps/dev-playground/client/src/routes/metrics.route.tsx +++ b/apps/dev-playground/client/src/routes/metrics.route.tsx @@ -113,17 +113,23 @@ function RevenueChart() { if (error) { return ( -
    -

    Could not load the revenue metric.

    -

    +

    +

    + Could not load the revenue metric. +

    +

    The dev workspace does not host the demo metric view at{" "} - appkit_demo.public.revenue_metrics. - The typed surface and metadata flow still compile — this panel would + + appkit_demo.public.revenue_metrics + + . The typed surface and metadata flow still compile — this panel would render a Plotly line chart with{" "} - $#,##0.00 tick formatting once the - metric view exists in your warehouse. + + $#,##0.00 + {" "} + tick formatting once the metric view exists in your warehouse.

    -

    Server error: {error}

    +

    {error}

    ); } @@ -220,32 +226,39 @@ function CustomerMetricsPanel({ user }: { user: string | null }) {
    )} {error && ( -
    -

    Could not load customer metrics.

    -

    +

    +

    + Could not load customer metrics. +

    +

    The dev workspace does not host the demo metric view at{" "} - appkit_demo.public.customer_metrics + + appkit_demo.public.customer_metrics + . When wired to a real OBO-lane metric view, this panel would show row-level scoping driven by{" "} - x-forwarded-access-token. + + x-forwarded-access-token + + .

    -

    Server error: {error}

    +

    {error}

    )} {data && data.length > 0 && ( - - - + + - - @@ -258,15 +271,18 @@ function CustomerMetricsPanel({ user }: { user: string | null }) { churn_rate: number; }> ).map((row) => ( - - - + + - .format access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /metrics demo route accessed `metadata?.measures.arr.format` and the sibling field for OBO measures via `metadata?.measures..format`. The optional-chain guards `metadata` itself, but a real-world bundle can ship with `measures: {}` (e.g., when `metric sync` couldn't extract columns from a particular view's DESCRIBE response — see the warning this commit adds upstream). In that case `metadata.measures.` is undefined and the `.format` access throws, the React ErrorBoundary catches the crash, and BOTH panels disappear behind a generic "Something went wrong" page even though only one panel is actually broken. Add `?.` between the column lookup and `.format`. formatLabel and formatValue / toD3Format already handle undefined gracefully (humanized fallback for labels, identity for tickformat), so the chart still renders — it just shows bare labels until the bundle is regenerated with column metadata. Also add a warn log to syncMetrics when extractMetricColumns succeeds but returns zero columns. Previously the path was silent; the only signal was a downstream React crash. Now misshapen DESCRIBE responses surface at sync time with a hint about the expected shape. Co-authored-by: Isaac Signed-off-by: Atila Fassina --- apps/dev-playground/client/package-lock.json | 2547 ++++++++++++++- .../client/src/routes/metrics.route.tsx | 10 +- .../shared/appkit-types/metric.d.ts | 62 +- .../shared/appkit-types/metrics.metadata.json | 44 +- .../src/type-generator/metric-registry.ts | 13 + pnpm-lock.yaml | 2907 +---------------- 6 files changed, 2579 insertions(+), 3004 deletions(-) diff --git a/apps/dev-playground/client/package-lock.json b/apps/dev-playground/client/package-lock.json index 80bd5ad40..80585ebd1 100644 --- a/apps/dev-playground/client/package-lock.json +++ b/apps/dev-playground/client/package-lock.json @@ -19,8 +19,10 @@ "class-variance-authority": "0.7.1", "clsx": "2.1.1", "lucide-react": "0.546.0", + "plotly.js": "^3.5.0", "react": "19.2.0", "react-dom": "19.2.0", + "react-plotly.js": "^2.6.0", "recharts": "3.4.1", "tailwind-merge": "3.3.1", "tailwindcss-animate": "1.0.7", @@ -33,6 +35,7 @@ "@types/node": "24.6.0", "@types/react": "19.2.2", "@types/react-dom": "19.2.2", + "@types/react-plotly.js": "^2.6.4", "@vitejs/plugin-react": "5.0.4", "autoprefixer": "10.4.21", "eslint": "9.36.0", @@ -522,6 +525,18 @@ "node": ">=6.9.0" } }, + "node_modules/@choojs/findup": { + "version": "0.2.1", + "resolved": "https://npm-proxy.dev.databricks.com/@choojs/findup/-/findup-0.2.1.tgz", + "integrity": "sha512-YstAqNb0MCN8PjdLCDfRsBcGVRN41f3vgLvaI0IrIcBp4AqILRSS0DeWNGkicC+f/zRIPJLc+9RURVSepwvfBw==", + "license": "MIT", + "dependencies": { + "commander": "^2.15.1" + }, + "bin": { + "findup": "bin/findup.js" + } + }, "node_modules/@emnapi/core": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", @@ -1258,6 +1273,110 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mapbox/geojson-rewind": { + "version": "0.5.2", + "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", + "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", + "license": "ISC", + "dependencies": { + "get-stream": "^6.0.1", + "minimist": "^1.2.6" + }, + "bin": { + "geojson-rewind": "geojson-rewind" + } + }, + "node_modules/@mapbox/geojson-types": { + "version": "1.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/geojson-types/-/geojson-types-1.0.2.tgz", + "integrity": "sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw==", + "license": "ISC" + }, + "node_modules/@mapbox/jsonlint-lines-primitives": { + "version": "2.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@mapbox/mapbox-gl-supported": { + "version": "1.5.0", + "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.5.0.tgz", + "integrity": "sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==", + "license": "BSD-3-Clause", + "peerDependencies": { + "mapbox-gl": ">=0.32.1 <2.0.0" + } + }, + "node_modules/@mapbox/point-geometry": { + "version": "0.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", + "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==", + "license": "ISC" + }, + "node_modules/@mapbox/tiny-sdf": { + "version": "1.2.5", + "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/tiny-sdf/-/tiny-sdf-1.2.5.tgz", + "integrity": "sha512-cD8A/zJlm6fdJOk6DqPUV8mcpyJkRz2x2R+/fYcWDYG3oWbG7/L7Yl/WqQ1VZCjnL9OTIMAn6c+BC5Eru4sQEw==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/unitbezier": { + "version": "0.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz", + "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/vector-tile": { + "version": "1.3.1", + "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", + "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "~0.1.0" + } + }, + "node_modules/@mapbox/whoots-js": { + "version": "3.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", + "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "20.4.0", + "resolved": "https://npm-proxy.dev.databricks.com/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.4.0.tgz", + "integrity": "sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==", + "license": "ISC", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/unitbezier": "^0.0.1", + "json-stringify-pretty-compact": "^4.0.0", + "minimist": "^1.2.8", + "quickselect": "^2.0.0", + "rw": "^1.3.3", + "tinyqueue": "^3.0.0" + }, + "bin": { + "gl-style-format": "dist/gl-style-format.mjs", + "gl-style-migrate": "dist/gl-style-migrate.mjs", + "gl-style-validate": "dist/gl-style-validate.mjs" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", + "license": "BSD-2-Clause" + }, + "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", @@ -1326,6 +1445,134 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@plotly/d3": { + "version": "3.8.2", + "resolved": "https://npm-proxy.dev.databricks.com/@plotly/d3/-/d3-3.8.2.tgz", + "integrity": "sha512-wvsNmh1GYjyJfyEBPKJLTMzgf2c2bEbSIL50lmqVUi+o1NHaLPi1Lb4v7VxXXJn043BhNyrxUrWI85Q+zmjOVA==", + "license": "BSD-3-Clause" + }, + "node_modules/@plotly/d3-sankey": { + "version": "0.7.2", + "resolved": "https://npm-proxy.dev.databricks.com/@plotly/d3-sankey/-/d3-sankey-0.7.2.tgz", + "integrity": "sha512-2jdVos1N3mMp3QW0k2q1ph7Gd6j5PY1YihBrwpkFnKqO+cqtZq3AdEYUeSGXMeLsBDQYiqTVcihYfk8vr5tqhw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1", + "d3-collection": "1", + "d3-shape": "^1.2.0" + } + }, + "node_modules/@plotly/d3-sankey-circular": { + "version": "0.33.1", + "resolved": "https://npm-proxy.dev.databricks.com/@plotly/d3-sankey-circular/-/d3-sankey-circular-0.33.1.tgz", + "integrity": "sha512-FgBV1HEvCr3DV7RHhDsPXyryknucxtfnLwPtCKKxdolKyTFYoLX/ibEfX39iFYIL7DYbVeRtP43dbFcrHNE+KQ==", + "license": "MIT", + "dependencies": { + "d3-array": "^1.2.1", + "d3-collection": "^1.0.4", + "d3-shape": "^1.2.0", + "elementary-circuits-directed-graph": "^1.0.4" + } + }, + "node_modules/@plotly/d3-sankey-circular/node_modules/d3-array": { + "version": "1.2.4", + "resolved": "https://npm-proxy.dev.databricks.com/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", + "license": "BSD-3-Clause" + }, + "node_modules/@plotly/d3-sankey-circular/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://npm-proxy.dev.databricks.com/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/@plotly/d3-sankey-circular/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://npm-proxy.dev.databricks.com/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/@plotly/d3-sankey/node_modules/d3-array": { + "version": "1.2.4", + "resolved": "https://npm-proxy.dev.databricks.com/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", + "license": "BSD-3-Clause" + }, + "node_modules/@plotly/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://npm-proxy.dev.databricks.com/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/@plotly/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://npm-proxy.dev.databricks.com/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/@plotly/mapbox-gl": { + "version": "1.13.4", + "resolved": "https://npm-proxy.dev.databricks.com/@plotly/mapbox-gl/-/mapbox-gl-1.13.4.tgz", + "integrity": "sha512-sR3/Pe5LqT/fhYgp4rT4aSFf1rTsxMbGiH6Hojc7PH36ny5Bn17iVFUjpzycafETURuFbLZUfjODO8LvSI+5zQ==", + "license": "SEE LICENSE IN LICENSE.txt", + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/geojson-types": "^1.0.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/mapbox-gl-supported": "^1.5.0", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^1.1.1", + "@mapbox/unitbezier": "^0.0.0", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "csscolorparser": "~1.0.3", + "earcut": "^2.2.2", + "geojson-vt": "^3.2.1", + "gl-matrix": "^3.2.1", + "grid-index": "^1.1.0", + "murmurhash-js": "^1.0.0", + "pbf": "^3.2.1", + "potpack": "^1.0.1", + "quickselect": "^2.0.0", + "rw": "^1.3.3", + "supercluster": "^7.1.0", + "tinyqueue": "^2.0.3", + "vt-pbf": "^3.1.1" + }, + "engines": { + "node": ">=6.4.0" + } + }, + "node_modules/@plotly/point-cluster": { + "version": "3.1.9", + "resolved": "https://npm-proxy.dev.databricks.com/@plotly/point-cluster/-/point-cluster-3.1.9.tgz", + "integrity": "sha512-MwaI6g9scKf68Orpr1pHZ597pYx9uP8UEFXLPbsCmuw3a84obwz6pnMXGc90VhgDNeNiLEdlmuK7CPo+5PIxXw==", + "license": "MIT", + "dependencies": { + "array-bounds": "^1.0.1", + "binary-search-bounds": "^2.0.4", + "clamp": "^1.0.1", + "defined": "^1.0.0", + "dtype": "^2.0.0", + "flatten-vertex-data": "^1.0.2", + "is-obj": "^1.0.1", + "math-log2": "^1.0.1", + "parse-rect": "^1.2.0", + "pick-by-alias": "^1.2.0" + } + }, + "node_modules/@plotly/regl": { + "version": "2.1.2", + "resolved": "https://npm-proxy.dev.databricks.com/@plotly/regl/-/regl-2.1.2.tgz", + "integrity": "sha512-Mdk+vUACbQvjd0m/1JJjOOafmkp/EpmHjISsopEz5Av44CBq7rPC05HHNbYGKVyNUF2zmEoBS/TT0pd0SPFFyw==", + "license": "MIT" + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -2900,6 +3147,78 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@turf/area": { + "version": "7.3.5", + "resolved": "https://npm-proxy.dev.databricks.com/@turf/area/-/area-7.3.5.tgz", + "integrity": "sha512-sSn80wPT7XfBIDN3vurCPxhk9W4U8ozS/XImSqeLN8qveTICOxzZkhsGDMp0CuncaN+plWut4a2TdNM7mzZB6Q==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@turf/meta": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/bbox": { + "version": "7.3.5", + "resolved": "https://npm-proxy.dev.databricks.com/@turf/bbox/-/bbox-7.3.5.tgz", + "integrity": "sha512-oG1ya/HtBjAIg4TimbWx+nOYPbY0bCvt82Bq8tm6sBw3qqtbOyRSfDz79Sq90TnH7DXJprJ1qnVGKNtZ6jemfw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@turf/meta": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/centroid": { + "version": "7.3.5", + "resolved": "https://npm-proxy.dev.databricks.com/@turf/centroid/-/centroid-7.3.5.tgz", + "integrity": "sha512-hkWaqwGFdOn6Tf0EWfn2yn1XZ1FWE1h2C5ZWstDMu/FxYO5DB+YjlmOFPl4K6SmSOEgdV07eK2vDCyPeTHqKGA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@turf/meta": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/helpers": { + "version": "7.3.5", + "resolved": "https://npm-proxy.dev.databricks.com/@turf/helpers/-/helpers-7.3.5.tgz", + "integrity": "sha512-E/NMGV5MwbjjP7AJXBtsanC3yY8N2MQ87IGdIgkB2ji5AtBpwnH4L3gEqpYN4RlCJJWbLbzO91BbKv2waUd0eg==", + "license": "MIT", + "dependencies": { + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/meta": { + "version": "7.3.5", + "resolved": "https://npm-proxy.dev.databricks.com/@turf/meta/-/meta-7.3.5.tgz", + "integrity": "sha512-r+ohqxoyqeigFB0oFrQx/YEHIkOKqcKpCjvZkvZs7Tkv+IFco5MezAd2zd4rzK+0DfFgDP3KpJc7HqrYjvEjhg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -3025,6 +3344,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://npm-proxy.dev.databricks.com/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/geojson-vt": { + "version": "3.2.5", + "resolved": "https://npm-proxy.dev.databricks.com/@types/geojson-vt/-/geojson-vt-3.2.5.tgz", + "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -3042,6 +3376,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mapbox__point-geometry": { + "version": "0.1.4", + "resolved": "https://npm-proxy.dev.databricks.com/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", + "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==", + "license": "MIT" + }, + "node_modules/@types/mapbox__vector-tile": { + "version": "1.3.4", + "resolved": "https://npm-proxy.dev.databricks.com/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz", + "integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*", + "@types/mapbox__point-geometry": "*", + "@types/pbf": "*" + } + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -3062,6 +3413,19 @@ "undici-types": "~7.13.0" } }, + "node_modules/@types/pbf": { + "version": "3.0.5", + "resolved": "https://npm-proxy.dev.databricks.com/@types/pbf/-/pbf-3.0.5.tgz", + "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==", + "license": "MIT" + }, + "node_modules/@types/plotly.js": { + "version": "3.0.10", + "resolved": "https://npm-proxy.dev.databricks.com/@types/plotly.js/-/plotly.js-3.0.10.tgz", + "integrity": "sha512-q+MgO4aajC2HrO7FllTYWzrpdfbTjboSMfjkz/aXKjg1v7HNo1zMEFfAW7quKfk6SL+bH74A5ThBEps/7hZxOA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", @@ -3082,6 +3446,26 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-plotly.js": { + "version": "2.6.4", + "resolved": "https://npm-proxy.dev.databricks.com/@types/react-plotly.js/-/react-plotly.js-2.6.4.tgz", + "integrity": "sha512-AU6w1u3qEGM0NmBA69PaOgNc0KPFA/+qkH6Uu9EBTJ45/WYOUoXi9AF5O15PRM2klpHSiHAAs4WnlI+OZAFmUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/plotly.js": "*", + "@types/react": "*" + } + }, + "node_modules/@types/supercluster": { + "version": "7.1.3", + "resolved": "https://npm-proxy.dev.databricks.com/@types/supercluster/-/supercluster-7.1.3.tgz", + "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -3394,6 +3778,12 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/abs-svg-path": { + "version": "0.1.1", + "resolved": "https://npm-proxy.dev.databricks.com/abs-svg-path/-/abs-svg-path-0.1.1.tgz", + "integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==", + "license": "MIT" + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -3490,6 +3880,36 @@ "node": ">=10" } }, + "node_modules/array-bounds": { + "version": "1.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/array-bounds/-/array-bounds-1.0.1.tgz", + "integrity": "sha512-8wdW3ZGk6UjMPJx/glyEt0sLzzwAE1bhToPsO1W2pbpR2gULyxe3BjSiuJFheP50T/GgODVPz2fuMUmIywt8cQ==", + "license": "MIT" + }, + "node_modules/array-find-index": { + "version": "1.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-normalize": { + "version": "1.1.4", + "resolved": "https://npm-proxy.dev.databricks.com/array-normalize/-/array-normalize-1.1.4.tgz", + "integrity": "sha512-fCp0wKFLjvSPmCn4F5Tiw4M3lpMZoHlCjfcs7nNzuj3vqQQ1/a8cgB9DXcpDSn18c+coLnaW7rqfcYCvKbyJXg==", + "license": "MIT", + "dependencies": { + "array-bounds": "^1.0.0" + } + }, + "node_modules/array-range": { + "version": "1.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/array-range/-/array-range-1.0.1.tgz", + "integrity": "sha512-shdaI1zT3CVNL2hnx9c0JMc0ZogGaxDs5e85akgHWKYa0yVbIyp06Ind3dVkTj/uuFrzaHBOyqFzo+VV6aXgtA==", + "license": "MIT" + }, "node_modules/ast-types": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", @@ -3559,6 +3979,15 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.8.16", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", @@ -3580,6 +4009,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/binary-search-bounds": { + "version": "2.0.5", + "resolved": "https://npm-proxy.dev.databricks.com/binary-search-bounds/-/binary-search-bounds-2.0.5.tgz", + "integrity": "sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==", + "license": "MIT" + }, + "node_modules/bit-twiddle": { + "version": "1.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/bit-twiddle/-/bit-twiddle-1.0.2.tgz", + "integrity": "sha512-B9UhK0DKFZhoTFcfvAzhqsjStvGJp9vYWf3+6SNTtdSQnvIgfkHbgHrg/e4+TH71N2GDu8tpmCVoyfrL1d7ntA==", + "license": "MIT" + }, + "node_modules/bitmap-sdf": { + "version": "1.0.4", + "resolved": "https://npm-proxy.dev.databricks.com/bitmap-sdf/-/bitmap-sdf-1.0.4.tgz", + "integrity": "sha512-1G3U4n5JE6RAiALMxu0p1XmeZkTeCwGKykzsLTCqVzfSDaN6S7fKnkIkfejogz+iwqBWc0UYAIKnKHNN7pSfDg==", + "license": "MIT" + }, + "node_modules/bl": { + "version": "2.2.1", + "resolved": "https://npm-proxy.dev.databricks.com/bl/-/bl-2.2.1.tgz", + "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -3636,6 +4093,12 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://npm-proxy.dev.databricks.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3666,6 +4129,15 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas-fit": { + "version": "1.5.0", + "resolved": "https://npm-proxy.dev.databricks.com/canvas-fit/-/canvas-fit-1.5.0.tgz", + "integrity": "sha512-onIcjRpz69/Hx5bB5HGbYKUF2uC6QT6Gp+pfpGm3A7mPfcluSLV5v4Zu+oflDUwLdUw0rLIBhUbi0v8hM4FJQQ==", + "license": "MIT", + "dependencies": { + "element-size": "^1.1.1" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -3752,6 +4224,12 @@ "node": ">= 6" } }, + "node_modules/clamp": { + "version": "1.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/clamp/-/clamp-1.0.1.tgz", + "integrity": "sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA==", + "license": "MIT" + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -3851,6 +4329,24 @@ "node": ">=6" } }, + "node_modules/color-alpha": { + "version": "1.0.4", + "resolved": "https://npm-proxy.dev.databricks.com/color-alpha/-/color-alpha-1.0.4.tgz", + "integrity": "sha512-lr8/t5NPozTSqli+duAN+x+no/2WaKTeWvxhHGN+aXT6AJ8vPlzLa7UriyjWak0pSC2jHol9JgjBYnnHsGha9A==", + "license": "MIT", + "dependencies": { + "color-parse": "^1.3.8" + } + }, + "node_modules/color-alpha/node_modules/color-parse": { + "version": "1.4.3", + "resolved": "https://npm-proxy.dev.databricks.com/color-parse/-/color-parse-1.4.3.tgz", + "integrity": "sha512-BADfVl/FHkQkyo8sRBwMYBqemqsgnu7JZAwUgvBvuwwuNUZAhSvLTbsEErS5bQXzOjDR0dWzJ4vXN2Q+QoPx0A==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3864,13 +4360,76 @@ "node": ">=7.0.0" } }, + "node_modules/color-id": { + "version": "1.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/color-id/-/color-id-1.1.0.tgz", + "integrity": "sha512-2iRtAn6dC/6/G7bBIo0uupVrIne1NsQJvJxZOBCzQOfk7jRq97feaDZ3RdzuHakRXXnHGNwglto3pqtRx1sX0g==", + "license": "MIT", + "dependencies": { + "clamp": "^1.0.1" + } + }, "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, + "node_modules/color-normalize": { + "version": "1.5.0", + "resolved": "https://npm-proxy.dev.databricks.com/color-normalize/-/color-normalize-1.5.0.tgz", + "integrity": "sha512-rUT/HDXMr6RFffrR53oX3HGWkDOP9goSAQGBkUaAYKjOE2JxozccdGyufageWDlInRAjm/jYPrf/Y38oa+7obw==", + "license": "MIT", + "dependencies": { + "clamp": "^1.0.1", + "color-rgba": "^2.1.1", + "dtype": "^2.0.0" + } + }, + "node_modules/color-normalize/node_modules/color-parse": { + "version": "1.4.3", + "resolved": "https://npm-proxy.dev.databricks.com/color-parse/-/color-parse-1.4.3.tgz", + "integrity": "sha512-BADfVl/FHkQkyo8sRBwMYBqemqsgnu7JZAwUgvBvuwwuNUZAhSvLTbsEErS5bQXzOjDR0dWzJ4vXN2Q+QoPx0A==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0" + } + }, + "node_modules/color-normalize/node_modules/color-rgba": { + "version": "2.4.0", + "resolved": "https://npm-proxy.dev.databricks.com/color-rgba/-/color-rgba-2.4.0.tgz", + "integrity": "sha512-Nti4qbzr/z2LbUWySr7H9dk3Rl7gZt7ihHAxlgT4Ho90EXWkjtkL1avTleu9yeGuqrt/chxTB6GKK8nZZ6V0+Q==", + "license": "MIT", + "dependencies": { + "color-parse": "^1.4.2", + "color-space": "^2.0.0" + } + }, + "node_modules/color-parse": { + "version": "2.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/color-parse/-/color-parse-2.0.0.tgz", + "integrity": "sha512-g2Z+QnWsdHLppAbrpcFWo629kLOnOPtpxYV69GCqm92gqSgyXbzlfyN3MXs0412fPBkFmiuS+rXposgBgBa6Kg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0" + } + }, + "node_modules/color-rgba": { + "version": "3.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/color-rgba/-/color-rgba-3.0.0.tgz", + "integrity": "sha512-PPwZYkEY3M2THEHHV6Y95sGUie77S7X8v+h1r6LSAPF3/LL2xJ8duUXSrkic31Nzc4odPwHgUbiX/XuTYzQHQg==", + "license": "MIT", + "dependencies": { + "color-parse": "^2.0.0", + "color-space": "^2.0.0" + } + }, + "node_modules/color-space": { + "version": "2.3.2", + "resolved": "https://npm-proxy.dev.databricks.com/color-space/-/color-space-2.3.2.tgz", + "integrity": "sha512-BcKnbOEsOarCwyoLstcoEztwT0IJxqqQkNwDuA3a65sICvvHL2yoeV13psoDFh5IuiOMnIOKdQDwB4Mk3BypiA==", + "license": "Unlicense" + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -3882,6 +4441,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://npm-proxy.dev.databricks.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3889,6 +4454,21 @@ "dev": true, "license": "MIT" }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://npm-proxy.dev.databricks.com/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -3901,6 +4481,18 @@ "integrity": "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://npm-proxy.dev.databricks.com/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/country-regex": { + "version": "1.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/country-regex/-/country-regex-1.1.0.tgz", + "integrity": "sha512-iSPlClZP8vX7MC3/u6s3lrDuoQyhQukh5LyABJ3hvfzbQ3Yyayd4fp04zjLnfi267B/B2FkumcWWgrbban7sSA==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3916,13 +4508,85 @@ "node": ">= 8" } }, + "node_modules/css-font": { + "version": "1.2.0", + "resolved": "https://npm-proxy.dev.databricks.com/css-font/-/css-font-1.2.0.tgz", + "integrity": "sha512-V4U4Wps4dPDACJ4WpgofJ2RT5Yqwe1lEH6wlOOaIxMi0gTjdIijsc5FmxQlZ7ZZyKQkkutqqvULOp07l9c7ssA==", + "license": "MIT", + "dependencies": { + "css-font-size-keywords": "^1.0.0", + "css-font-stretch-keywords": "^1.0.1", + "css-font-style-keywords": "^1.0.1", + "css-font-weight-keywords": "^1.0.0", + "css-global-keywords": "^1.0.1", + "css-system-font-keywords": "^1.0.0", + "pick-by-alias": "^1.2.0", + "string-split-by": "^1.0.0", + "unquote": "^1.1.0" + } + }, + "node_modules/css-font-size-keywords": { + "version": "1.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/css-font-size-keywords/-/css-font-size-keywords-1.0.0.tgz", + "integrity": "sha512-Q+svMDbMlelgCfH/RVDKtTDaf5021O486ZThQPIpahnIjUkMUslC+WuOQSWTgGSrNCH08Y7tYNEmmy0hkfMI8Q==", + "license": "MIT" + }, + "node_modules/css-font-stretch-keywords": { + "version": "1.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/css-font-stretch-keywords/-/css-font-stretch-keywords-1.0.1.tgz", + "integrity": "sha512-KmugPO2BNqoyp9zmBIUGwt58UQSfyk1X5DbOlkb2pckDXFSAfjsD5wenb88fNrD6fvS+vu90a/tsPpb9vb0SLg==", + "license": "MIT" + }, + "node_modules/css-font-style-keywords": { + "version": "1.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/css-font-style-keywords/-/css-font-style-keywords-1.0.1.tgz", + "integrity": "sha512-0Fn0aTpcDktnR1RzaBYorIxQily85M2KXRpzmxQPgh8pxUN9Fcn00I8u9I3grNr1QXVgCl9T5Imx0ZwKU973Vg==", + "license": "MIT" + }, + "node_modules/css-font-weight-keywords": { + "version": "1.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/css-font-weight-keywords/-/css-font-weight-keywords-1.0.0.tgz", + "integrity": "sha512-5So8/NH+oDD+EzsnF4iaG4ZFHQ3vaViePkL1ZbZ5iC/KrsCY+WHq/lvOgrtmuOQ9pBBZ1ADGpaf+A4lj1Z9eYA==", + "license": "MIT" + }, + "node_modules/css-global-keywords": { + "version": "1.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/css-global-keywords/-/css-global-keywords-1.0.1.tgz", + "integrity": "sha512-X1xgQhkZ9n94WDwntqst5D/FKkmiU0GlJSFZSV3kLvyJ1WC5VeyoXDOuleUD+SIuH9C7W05is++0Woh0CGfKjQ==", + "license": "MIT" + }, + "node_modules/css-system-font-keywords": { + "version": "1.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/css-system-font-keywords/-/css-system-font-keywords-1.0.0.tgz", + "integrity": "sha512-1umTtVd/fXS25ftfjB71eASCrYhilmEsvDEI6wG/QplnmlfmVM5HkZ/ZX46DT5K3eblFPgLUHt5BRCb0YXkSFA==", + "license": "MIT" + }, + "node_modules/csscolorparser": { + "version": "1.0.3", + "resolved": "https://npm-proxy.dev.databricks.com/csscolorparser/-/csscolorparser-1.0.3.tgz", + "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==", + "license": "MIT" + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, - "node_modules/d3-array": { + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", @@ -3934,6 +4598,12 @@ "node": ">=12" } }, + "node_modules/d3-collection": { + "version": "1.0.7", + "resolved": "https://npm-proxy.dev.databricks.com/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==", + "license": "BSD-3-Clause" + }, "node_modules/d3-color": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", @@ -3943,6 +4613,12 @@ "node": ">=12" } }, + "node_modules/d3-dispatch": { + "version": "1.0.6", + "resolved": "https://npm-proxy.dev.databricks.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz", + "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==", + "license": "BSD-3-Clause" + }, "node_modules/d3-ease": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", @@ -3952,6 +4628,24 @@ "node": ">=12" } }, + "node_modules/d3-force": { + "version": "1.2.1", + "resolved": "https://npm-proxy.dev.databricks.com/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "node_modules/d3-force/node_modules/d3-timer": { + "version": "1.0.10", + "resolved": "https://npm-proxy.dev.databricks.com/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==", + "license": "BSD-3-Clause" + }, "node_modules/d3-format": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", @@ -3961,6 +4655,52 @@ "node": ">=12" } }, + "node_modules/d3-geo": { + "version": "1.12.1", + "resolved": "https://npm-proxy.dev.databricks.com/d3-geo/-/d3-geo-1.12.1.tgz", + "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1" + } + }, + "node_modules/d3-geo-projection": { + "version": "2.9.0", + "resolved": "https://npm-proxy.dev.databricks.com/d3-geo-projection/-/d3-geo-projection-2.9.0.tgz", + "integrity": "sha512-ZULvK/zBn87of5rWAfFMc9mJOipeSo57O+BBitsKIXmU4rTVAnX1kSsJkE0R+TxY8pGNoM1nbyRRE7GYHhdOEQ==", + "license": "BSD-3-Clause", + "dependencies": { + "commander": "2", + "d3-array": "1", + "d3-geo": "^1.12.0", + "resolve": "^1.1.10" + }, + "bin": { + "geo2svg": "bin/geo2svg", + "geograticule": "bin/geograticule", + "geoproject": "bin/geoproject", + "geoquantize": "bin/geoquantize", + "geostitch": "bin/geostitch" + } + }, + "node_modules/d3-geo-projection/node_modules/d3-array": { + "version": "1.2.4", + "resolved": "https://npm-proxy.dev.databricks.com/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-geo/node_modules/d3-array": { + "version": "1.2.4", + "resolved": "https://npm-proxy.dev.databricks.com/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://npm-proxy.dev.databricks.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==", + "license": "BSD-3-Clause" + }, "node_modules/d3-interpolate": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", @@ -3982,6 +4722,12 @@ "node": ">=12" } }, + "node_modules/d3-quadtree": { + "version": "1.0.7", + "resolved": "https://npm-proxy.dev.databricks.com/d3-quadtree/-/d3-quadtree-1.0.7.tgz", + "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==", + "license": "BSD-3-Clause" + }, "node_modules/d3-scale": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", @@ -4073,6 +4819,15 @@ "dev": true, "license": "MIT" }, + "node_modules/defined": { + "version": "1.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/defined/-/defined-1.0.1.tgz", + "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -4083,6 +4838,12 @@ "node": ">=6" } }, + "node_modules/detect-kerning": { + "version": "2.1.2", + "resolved": "https://npm-proxy.dev.databricks.com/detect-kerning/-/detect-kerning-2.1.2.tgz", + "integrity": "sha512-I3JIbrnKPAntNLl1I6TpSQQdQ4AutYzv/sKMFKbepawV/hlH0GmYKhUoOEMd4xqaUHT+Bm0f4127lh5qs1m1tw==", + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -4121,12 +4882,79 @@ "node": ">=0.3.1" } }, + "node_modules/draw-svg-path": { + "version": "1.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/draw-svg-path/-/draw-svg-path-1.0.0.tgz", + "integrity": "sha512-P8j3IHxcgRMcY6sDzr0QvJDLzBnJJqpTG33UZ2Pvp8rw0apCHhJCWqYprqrXjrgHnJ6tuhP1iTJSAodPDHxwkg==", + "license": "MIT", + "dependencies": { + "abs-svg-path": "~0.1.1", + "normalize-svg-path": "~0.1.0" + } + }, + "node_modules/dtype": { + "version": "2.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/dtype/-/dtype-2.0.0.tgz", + "integrity": "sha512-s2YVcLKdFGS0hpFqJaTwscsyt0E8nNFdmo73Ocd81xNPj4URI4rj6D60A+vFMIw7BXWlb4yRkEwfBqcZzPGiZg==", + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/dup": { + "version": "1.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/dup/-/dup-1.0.0.tgz", + "integrity": "sha512-Bz5jxMMC0wgp23Zm15ip1x8IhYRqJvF3nFC0UInJUDkN1z4uNPk9jTnfCUJXbOGiQ1JbXLQsiV41Fb+HXcj5BA==", + "license": "MIT" + }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://npm-proxy.dev.databricks.com/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/earcut": { + "version": "2.2.4", + "resolved": "https://npm-proxy.dev.databricks.com/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", + "license": "ISC" + }, "node_modules/electron-to-chromium": { "version": "1.5.237", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz", "integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==", "license": "ISC" }, + "node_modules/element-size": { + "version": "1.1.1", + "resolved": "https://npm-proxy.dev.databricks.com/element-size/-/element-size-1.1.1.tgz", + "integrity": "sha512-eaN+GMOq/Q+BIWy0ybsgpcYImjGIdNLyjLFJU4XsLHXYQao5jCNb36GyN6C2qwmDDYSfIBmKpPpr4VnBdLCsPQ==", + "license": "MIT" + }, + "node_modules/elementary-circuits-directed-graph": { + "version": "1.3.1", + "resolved": "https://npm-proxy.dev.databricks.com/elementary-circuits-directed-graph/-/elementary-circuits-directed-graph-1.3.1.tgz", + "integrity": "sha512-ZEiB5qkn2adYmpXGnJKkxT8uJHlW/mxmBpmeqawEHzPxh9HkLD4/1mFYX5l0On+f6rcPIt8/EWlRU2Vo3fX6dQ==", + "license": "MIT", + "dependencies": { + "strongly-connected-components": "^1.0.1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://npm-proxy.dev.databricks.com/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", @@ -4141,6 +4969,15 @@ "node": ">=10.13.0" } }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://npm-proxy.dev.databricks.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-toolkit": { "version": "1.42.0", "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.42.0.tgz", @@ -4151,6 +4988,58 @@ "benchmarks" ] }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://npm-proxy.dev.databricks.com/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://npm-proxy.dev.databricks.com/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://npm-proxy.dev.databricks.com/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://npm-proxy.dev.databricks.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -4214,6 +5103,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://npm-proxy.dev.databricks.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/eslint": { "version": "9.36.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz", @@ -4328,6 +5248,21 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -4389,7 +5324,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -4399,18 +5333,70 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://npm-proxy.dev.databricks.com/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://npm-proxy.dev.databricks.com/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://npm-proxy.dev.databricks.com/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/falafel": { + "version": "2.2.5", + "resolved": "https://npm-proxy.dev.databricks.com/falafel/-/falafel-2.2.5.tgz", + "integrity": "sha512-HuC1qF9iTnHDnML9YZAdCDQwT0yKl/U55K4XSUXqGAA2GLoafFgWRqdAbhWJxXaYD4pyoVxAJ8wH670jMpI9DQ==", + "license": "MIT", + "dependencies": { + "acorn": "^7.1.1", + "isarray": "^2.0.1" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/falafel/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://npm-proxy.dev.databricks.com/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4448,6 +5434,15 @@ "node": ">= 6" } }, + "node_modules/fast-isnumeric": { + "version": "1.1.4", + "resolved": "https://npm-proxy.dev.databricks.com/fast-isnumeric/-/fast-isnumeric-1.1.4.tgz", + "integrity": "sha512-1mM8qOr2LYz8zGaUdmiqRDiuue00Dxjgcb1NQR7TnhLVh6sQyngP9xvLo7Sl7LZpP/sk5eb+bcyWXw530NTBZw==", + "license": "MIT", + "dependencies": { + "is-string-blank": "^1.0.1" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -4535,6 +5530,33 @@ "dev": true, "license": "ISC" }, + "node_modules/flatten-vertex-data": { + "version": "1.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/flatten-vertex-data/-/flatten-vertex-data-1.0.2.tgz", + "integrity": "sha512-BvCBFK2NZqerFTdMDgqfHBwxYWnxeCkwONsw6PvBMcUXqo8U/KDWwmXhqx1x2kLIg7DqIsJfOaJFOmlua3Lxuw==", + "license": "MIT", + "dependencies": { + "dtype": "^2.0.0" + } + }, + "node_modules/font-atlas": { + "version": "2.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/font-atlas/-/font-atlas-2.1.0.tgz", + "integrity": "sha512-kP3AmvX+HJpW4w3d+PiPR2X6E1yvsBXt2yhuCw+yReO9F1WYhvZwx3c95DGZGwg9xYzDGrgJYa885xmVA+28Cg==", + "license": "MIT", + "dependencies": { + "css-font": "^1.0.0" + } + }, + "node_modules/font-measure": { + "version": "1.2.2", + "resolved": "https://npm-proxy.dev.databricks.com/font-measure/-/font-measure-1.2.2.tgz", + "integrity": "sha512-mRLEpdrWzKe9hbfaF3Qpr06TAjquuBVP5cHy4b3hyeNdjc9i0PO6HniGsX5vjL5OWv7+Bd++NiooNpT/s8BvIA==", + "license": "MIT", + "dependencies": { + "css-font": "^1.2.0" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -4549,6 +5571,16 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://npm-proxy.dev.databricks.com/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4563,6 +5595,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://npm-proxy.dev.databricks.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -4572,6 +5613,12 @@ "node": ">=6.9.0" } }, + "node_modules/geojson-vt": { + "version": "3.2.1", + "resolved": "https://npm-proxy.dev.databricks.com/geojson-vt/-/geojson-vt-3.2.1.tgz", + "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==", + "license": "ISC" + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -4582,6 +5629,12 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-canvas-context": { + "version": "1.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/get-canvas-context/-/get-canvas-context-1.0.2.tgz", + "integrity": "sha512-LnpfLf/TNzr9zVOGiIY6aKCz8EKuXmlYNV7CM2pUjBa/B+c2I15tS7KLySep75+FuerJdmArvJLcsAXWEy2H0A==", + "license": "MIT" + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -4591,6 +5644,18 @@ "node": ">=6" } }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-tsconfig": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", @@ -4603,6 +5668,58 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/gl-mat4": { + "version": "1.2.0", + "resolved": "https://npm-proxy.dev.databricks.com/gl-mat4/-/gl-mat4-1.2.0.tgz", + "integrity": "sha512-sT5C0pwB1/e9G9AvAoLsoaJtbMGjfd/jfxo8jMCKqYYEnjZuFvqV5rehqar0538EmssjdDeiEWnKyBSTw7quoA==", + "license": "Zlib" + }, + "node_modules/gl-matrix": { + "version": "3.4.4", + "resolved": "https://npm-proxy.dev.databricks.com/gl-matrix/-/gl-matrix-3.4.4.tgz", + "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", + "license": "MIT" + }, + "node_modules/gl-text": { + "version": "1.4.0", + "resolved": "https://npm-proxy.dev.databricks.com/gl-text/-/gl-text-1.4.0.tgz", + "integrity": "sha512-o47+XBqLCj1efmuNyCHt7/UEJmB9l66ql7pnobD6p+sgmBUdzfMZXIF0zD2+KRfpd99DJN+QXdvTFAGCKCVSmQ==", + "license": "MIT", + "dependencies": { + "bit-twiddle": "^1.0.2", + "color-normalize": "^1.5.0", + "css-font": "^1.2.0", + "detect-kerning": "^2.1.2", + "es6-weak-map": "^2.0.3", + "flatten-vertex-data": "^1.0.2", + "font-atlas": "^2.1.0", + "font-measure": "^1.2.2", + "gl-util": "^3.1.2", + "is-plain-obj": "^1.1.0", + "object-assign": "^4.1.1", + "parse-rect": "^1.2.0", + "parse-unit": "^1.0.1", + "pick-by-alias": "^1.2.0", + "regl": "^2.0.0", + "to-px": "^1.0.1", + "typedarray-pool": "^1.1.0" + } + }, + "node_modules/gl-util": { + "version": "3.1.3", + "resolved": "https://npm-proxy.dev.databricks.com/gl-util/-/gl-util-3.1.3.tgz", + "integrity": "sha512-dvRTggw5MSkJnCbh74jZzSoTOGnVYK+Bt+Ckqm39CVcl6+zSsxqWk4lr5NKhkqXHL6qvZAU9h17ZF8mIskY9mA==", + "license": "MIT", + "dependencies": { + "is-browser": "^2.0.1", + "is-firefox": "^1.0.3", + "is-plain-obj": "^1.1.0", + "number-is-integer": "^1.0.1", + "object-assign": "^4.1.0", + "pick-by-alias": "^1.2.0", + "weak-map": "^1.0.5" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4616,6 +5733,44 @@ "node": ">=10.13.0" } }, + "node_modules/global-prefix": { + "version": "4.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/global-prefix/-/global-prefix-4.0.0.tgz", + "integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==", + "license": "MIT", + "dependencies": { + "ini": "^4.1.3", + "kind-of": "^6.0.3", + "which": "^4.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/global-prefix/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://npm-proxy.dev.databricks.com/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "4.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, "node_modules/globals": { "version": "16.4.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", @@ -4629,6 +5784,207 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/glsl-inject-defines": { + "version": "1.0.3", + "resolved": "https://npm-proxy.dev.databricks.com/glsl-inject-defines/-/glsl-inject-defines-1.0.3.tgz", + "integrity": "sha512-W49jIhuDtF6w+7wCMcClk27a2hq8znvHtlGnrYkSWEr8tHe9eA2dcnohlcAmxLYBSpSSdzOkRdyPTrx9fw49+A==", + "license": "MIT", + "dependencies": { + "glsl-token-inject-block": "^1.0.0", + "glsl-token-string": "^1.0.1", + "glsl-tokenizer": "^2.0.2" + } + }, + "node_modules/glsl-resolve": { + "version": "0.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/glsl-resolve/-/glsl-resolve-0.0.1.tgz", + "integrity": "sha512-xxFNsfnhZTK9NBhzJjSBGX6IOqYpvBHxxmo+4vapiljyGNCY0Bekzn0firQkQrazK59c1hYxMDxYS8MDlhw4gA==", + "license": "MIT", + "dependencies": { + "resolve": "^0.6.1", + "xtend": "^2.1.2" + } + }, + "node_modules/glsl-resolve/node_modules/resolve": { + "version": "0.6.3", + "resolved": "https://npm-proxy.dev.databricks.com/resolve/-/resolve-0.6.3.tgz", + "integrity": "sha512-UHBY3viPlJKf85YijDUcikKX6tmF4SokIDp518ZDVT92JNDcG5uKIthaT/owt3Sar0lwtOafsQuwrg22/v2Dwg==", + "license": "MIT" + }, + "node_modules/glsl-resolve/node_modules/xtend": { + "version": "2.2.0", + "resolved": "https://npm-proxy.dev.databricks.com/xtend/-/xtend-2.2.0.tgz", + "integrity": "sha512-SLt5uylT+4aoXxXuwtQp5ZnMMzhDb1Xkg4pEqc00WUJCQifPfV9Ub1VrNhp9kXkrjZD2I2Hl8WnjP37jzZLPZw==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/glsl-token-assignments": { + "version": "2.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/glsl-token-assignments/-/glsl-token-assignments-2.0.2.tgz", + "integrity": "sha512-OwXrxixCyHzzA0U2g4btSNAyB2Dx8XrztY5aVUCjRSh4/D0WoJn8Qdps7Xub3sz6zE73W3szLrmWtQ7QMpeHEQ==", + "license": "MIT" + }, + "node_modules/glsl-token-defines": { + "version": "1.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/glsl-token-defines/-/glsl-token-defines-1.0.0.tgz", + "integrity": "sha512-Vb5QMVeLjmOwvvOJuPNg3vnRlffscq2/qvIuTpMzuO/7s5kT+63iL6Dfo2FYLWbzuiycWpbC0/KV0biqFwHxaQ==", + "license": "MIT", + "dependencies": { + "glsl-tokenizer": "^2.0.0" + } + }, + "node_modules/glsl-token-depth": { + "version": "1.1.2", + "resolved": "https://npm-proxy.dev.databricks.com/glsl-token-depth/-/glsl-token-depth-1.1.2.tgz", + "integrity": "sha512-eQnIBLc7vFf8axF9aoi/xW37LSWd2hCQr/3sZui8aBJnksq9C7zMeUYHVJWMhFzXrBU7fgIqni4EhXVW4/krpg==", + "license": "MIT" + }, + "node_modules/glsl-token-descope": { + "version": "1.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/glsl-token-descope/-/glsl-token-descope-1.0.2.tgz", + "integrity": "sha512-kS2PTWkvi/YOeicVjXGgX5j7+8N7e56srNDEHDTVZ1dcESmbmpmgrnpjPcjxJjMxh56mSXYoFdZqb90gXkGjQw==", + "license": "MIT", + "dependencies": { + "glsl-token-assignments": "^2.0.0", + "glsl-token-depth": "^1.1.0", + "glsl-token-properties": "^1.0.0", + "glsl-token-scope": "^1.1.0" + } + }, + "node_modules/glsl-token-inject-block": { + "version": "1.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/glsl-token-inject-block/-/glsl-token-inject-block-1.1.0.tgz", + "integrity": "sha512-q/m+ukdUBuHCOtLhSr0uFb/qYQr4/oKrPSdIK2C4TD+qLaJvqM9wfXIF/OOBjuSA3pUoYHurVRNao6LTVVUPWA==", + "license": "MIT" + }, + "node_modules/glsl-token-properties": { + "version": "1.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/glsl-token-properties/-/glsl-token-properties-1.0.1.tgz", + "integrity": "sha512-dSeW1cOIzbuUoYH0y+nxzwK9S9O3wsjttkq5ij9ZGw0OS41BirKJzzH48VLm8qLg+au6b0sINxGC0IrGwtQUcA==", + "license": "MIT" + }, + "node_modules/glsl-token-scope": { + "version": "1.1.2", + "resolved": "https://npm-proxy.dev.databricks.com/glsl-token-scope/-/glsl-token-scope-1.1.2.tgz", + "integrity": "sha512-YKyOMk1B/tz9BwYUdfDoHvMIYTGtVv2vbDSLh94PT4+f87z21FVdou1KNKgF+nECBTo0fJ20dpm0B1vZB1Q03A==", + "license": "MIT" + }, + "node_modules/glsl-token-string": { + "version": "1.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/glsl-token-string/-/glsl-token-string-1.0.1.tgz", + "integrity": "sha512-1mtQ47Uxd47wrovl+T6RshKGkRRCYWhnELmkEcUAPALWGTFe2XZpH3r45XAwL2B6v+l0KNsCnoaZCSnhzKEksg==", + "license": "MIT" + }, + "node_modules/glsl-token-whitespace-trim": { + "version": "1.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/glsl-token-whitespace-trim/-/glsl-token-whitespace-trim-1.0.0.tgz", + "integrity": "sha512-ZJtsPut/aDaUdLUNtmBYhaCmhIjpKNg7IgZSfX5wFReMc2vnj8zok+gB/3Quqs0TsBSX/fGnqUUYZDqyuc2xLQ==", + "license": "MIT" + }, + "node_modules/glsl-tokenizer": { + "version": "2.1.5", + "resolved": "https://npm-proxy.dev.databricks.com/glsl-tokenizer/-/glsl-tokenizer-2.1.5.tgz", + "integrity": "sha512-XSZEJ/i4dmz3Pmbnpsy3cKh7cotvFlBiZnDOwnj/05EwNp2XrhQ4XKJxT7/pDt4kp4YcpRSKz8eTV7S+mwV6MA==", + "license": "MIT", + "dependencies": { + "through2": "^0.6.3" + } + }, + "node_modules/glsl-tokenizer/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/glsl-tokenizer/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://npm-proxy.dev.databricks.com/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/glsl-tokenizer/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://npm-proxy.dev.databricks.com/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "license": "MIT" + }, + "node_modules/glsl-tokenizer/node_modules/through2": { + "version": "0.6.5", + "resolved": "https://npm-proxy.dev.databricks.com/through2/-/through2-0.6.5.tgz", + "integrity": "sha512-RkK/CCESdTKQZHdmKICijdKKsCRVHs5KsLZ6pACAmF/1GPUQhonHSXWNERctxEp7RmvjdNbZTL5z9V7nSCXKcg==", + "license": "MIT", + "dependencies": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + }, + "node_modules/glslify": { + "version": "7.1.1", + "resolved": "https://npm-proxy.dev.databricks.com/glslify/-/glslify-7.1.1.tgz", + "integrity": "sha512-bud98CJ6kGZcP9Yxcsi7Iz647wuDz3oN+IZsjCRi5X1PI7t/xPKeL0mOwXJjo+CRZMqvq0CkSJiywCcY7kVYog==", + "license": "MIT", + "dependencies": { + "bl": "^2.2.1", + "concat-stream": "^1.5.2", + "duplexify": "^3.4.5", + "falafel": "^2.1.0", + "from2": "^2.3.0", + "glsl-resolve": "0.0.1", + "glsl-token-whitespace-trim": "^1.0.0", + "glslify-bundle": "^5.0.0", + "glslify-deps": "^1.2.5", + "minimist": "^1.2.5", + "resolve": "^1.1.5", + "stack-trace": "0.0.9", + "static-eval": "^2.0.5", + "through2": "^2.0.1", + "xtend": "^4.0.0" + }, + "bin": { + "glslify": "bin.js" + } + }, + "node_modules/glslify-bundle": { + "version": "5.1.1", + "resolved": "https://npm-proxy.dev.databricks.com/glslify-bundle/-/glslify-bundle-5.1.1.tgz", + "integrity": "sha512-plaAOQPv62M1r3OsWf2UbjN0hUYAB7Aph5bfH58VxJZJhloRNbxOL9tl/7H71K7OLJoSJ2ZqWOKk3ttQ6wy24A==", + "license": "MIT", + "dependencies": { + "glsl-inject-defines": "^1.0.1", + "glsl-token-defines": "^1.0.0", + "glsl-token-depth": "^1.1.1", + "glsl-token-descope": "^1.0.2", + "glsl-token-scope": "^1.1.1", + "glsl-token-string": "^1.0.1", + "glsl-token-whitespace-trim": "^1.0.0", + "glsl-tokenizer": "^2.0.2", + "murmurhash-js": "^1.0.0", + "shallow-copy": "0.0.1" + } + }, + "node_modules/glslify-deps": { + "version": "1.3.2", + "resolved": "https://npm-proxy.dev.databricks.com/glslify-deps/-/glslify-deps-1.3.2.tgz", + "integrity": "sha512-7S7IkHWygJRjcawveXQjRXLO2FTjijPDYC7QfZyAQanY+yGLCFHYnPtsGT9bdyHiwPTw/5a1m1M9hamT2aBpag==", + "license": "ISC", + "dependencies": { + "@choojs/findup": "^0.2.0", + "events": "^3.2.0", + "glsl-resolve": "0.0.1", + "glsl-tokenizer": "^2.0.0", + "graceful-fs": "^4.1.2", + "inherits": "^2.0.1", + "map-limit": "0.0.1", + "resolve": "^1.0.0" + } + }, "node_modules/goober": { "version": "2.1.18", "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", @@ -4642,7 +5998,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -4652,6 +6007,12 @@ "dev": true, "license": "MIT" }, + "node_modules/grid-index": { + "version": "1.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/grid-index/-/grid-index-1.1.0.tgz", + "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==", + "license": "ISC" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -4662,6 +6023,36 @@ "node": ">=8" } }, + "node_modules/has-hover": { + "version": "1.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/has-hover/-/has-hover-1.0.1.tgz", + "integrity": "sha512-0G6w7LnlcpyDzpeGUTuT0CEw05+QlMuGVk1IHNAlHrGJITGodjZu3x8BNDUMfKJSZXNB2ZAclqc1bvrd+uUpfg==", + "license": "MIT", + "dependencies": { + "is-browser": "^2.0.1" + } + }, + "node_modules/has-passive-events": { + "version": "1.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/has-passive-events/-/has-passive-events-1.0.0.tgz", + "integrity": "sha512-2vSj6IeIsgvsRMyeQ0JaCX5Q3lX4zMn5HpoVc7MEhQ6pv8Iq9rsXjsp+E5ZwaT7T0xhMT0KmU8gtt1EFVdbJiw==", + "license": "MIT", + "dependencies": { + "is-browser": "^2.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://npm-proxy.dev.databricks.com/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hast-util-to-html": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", @@ -4711,6 +6102,38 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://npm-proxy.dev.databricks.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://npm-proxy.dev.databricks.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4758,6 +6181,21 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://npm-proxy.dev.databricks.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://npm-proxy.dev.databricks.com/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -4779,6 +6217,27 @@ "node": ">=8" } }, + "node_modules/is-browser": { + "version": "2.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/is-browser/-/is-browser-2.1.0.tgz", + "integrity": "sha512-F5rTJxDQ2sW81fcfOR1GnCXT6sVJC104fCyfj+mjpwNEwaPYSn5fte5jiHmBg3DHsIoL/l8Kvw5VN5SsTRcRFQ==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://npm-proxy.dev.databricks.com/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4788,6 +6247,27 @@ "node": ">=0.10.0" } }, + "node_modules/is-finite": { + "version": "1.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-firefox": { + "version": "1.0.3", + "resolved": "https://npm-proxy.dev.databricks.com/is-firefox/-/is-firefox-1.0.3.tgz", + "integrity": "sha512-6Q9ITjvWIm0Xdqv+5U12wgOKEM2KoBw4Y926m0OFkvlCxnbG94HKAsVz8w3fWcfAS5YA2fJORXX1dLrkprCCxA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -4810,6 +6290,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-mobile": { + "version": "4.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/is-mobile/-/is-mobile-4.0.0.tgz", + "integrity": "sha512-mlcHZA84t1qLSuWkt2v0I2l61PYdyQDt4aG1mLIXF5FDMm4+haBCxCPYSr/uwqQNRk1MiTizn0ypEuRAOLRAew==", + "license": "MIT" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -4819,6 +6305,42 @@ "node": ">=0.12.0" } }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-string-blank": { + "version": "1.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/is-string-blank/-/is-string-blank-1.0.1.tgz", + "integrity": "sha512-9H+ZBCVs3L9OYqv8nuUAzpcT9OTgMD1yAWrG7ihlnibdkbtB850heAmYWxHuXc4CHy4lKeK69tN+ny1K7gBIrw==", + "license": "MIT" + }, + "node_modules/is-svg-path": { + "version": "1.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/is-svg-path/-/is-svg-path-1.0.2.tgz", + "integrity": "sha512-Lj4vePmqpPR1ZnRctHv8ltSh1OrSxHkhUkd7wi+VQdcdP15/KvQFyk7LhNuM7ZW0EVbJz8kZLVmL9quLrfq4Kg==", + "license": "MIT" + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://npm-proxy.dev.databricks.com/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, "node_modules/isbot": { "version": "5.1.31", "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.31.tgz", @@ -4897,6 +6419,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -4909,6 +6437,12 @@ "node": ">=6" } }, + "node_modules/kdbush": { + "version": "4.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/kdbush/-/kdbush-4.0.2.tgz", + "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", + "license": "ISC" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4919,6 +6453,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://npm-proxy.dev.databricks.com/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5202,9 +6745,20 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://npm-proxy.dev.databricks.com/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -5233,6 +6787,159 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/map-limit": { + "version": "0.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/map-limit/-/map-limit-0.0.1.tgz", + "integrity": "sha512-pJpcfLPnIF/Sk3taPW21G/RQsEEirGaFpCW3oXRwH9dnFHPHNGjNyvh++rdmC2fNqEaTw2MhYJraoJWAHx8kEg==", + "license": "MIT", + "dependencies": { + "once": "~1.3.0" + } + }, + "node_modules/map-limit/node_modules/once": { + "version": "1.3.3", + "resolved": "https://npm-proxy.dev.databricks.com/once/-/once-1.3.3.tgz", + "integrity": "sha512-6vaNInhu+CHxtONf3zw3vq4SP2DOQhjBvIa3rNcG0+P7eKWlYH6Peu7rHizSloRU2EwMz6GraLieis9Ac9+p1w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/mapbox-gl": { + "version": "1.13.3", + "resolved": "https://npm-proxy.dev.databricks.com/mapbox-gl/-/mapbox-gl-1.13.3.tgz", + "integrity": "sha512-p8lJFEiqmEQlyv+DQxFAOG/XPWN0Wp7j/Psq93Zywz7qt9CcUKFYDBOoOEKzqe6gudHVJY8/Bhqw6VDpX2lSBg==", + "license": "SEE LICENSE IN LICENSE.txt", + "peer": true, + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/geojson-types": "^1.0.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/mapbox-gl-supported": "^1.5.0", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^1.1.1", + "@mapbox/unitbezier": "^0.0.0", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "csscolorparser": "~1.0.3", + "earcut": "^2.2.2", + "geojson-vt": "^3.2.1", + "gl-matrix": "^3.2.1", + "grid-index": "^1.1.0", + "murmurhash-js": "^1.0.0", + "pbf": "^3.2.1", + "potpack": "^1.0.1", + "quickselect": "^2.0.0", + "rw": "^1.3.3", + "supercluster": "^7.1.0", + "tinyqueue": "^2.0.3", + "vt-pbf": "^3.1.1" + }, + "engines": { + "node": ">=6.4.0" + } + }, + "node_modules/maplibre-gl": { + "version": "4.7.1", + "resolved": "https://npm-proxy.dev.databricks.com/maplibre-gl/-/maplibre-gl-4.7.1.tgz", + "integrity": "sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^2.0.6", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "@maplibre/maplibre-gl-style-spec": "^20.3.1", + "@types/geojson": "^7946.0.14", + "@types/geojson-vt": "3.2.5", + "@types/mapbox__point-geometry": "^0.1.4", + "@types/mapbox__vector-tile": "^1.3.4", + "@types/pbf": "^3.0.5", + "@types/supercluster": "^7.1.3", + "earcut": "^3.0.0", + "geojson-vt": "^4.0.2", + "gl-matrix": "^3.4.3", + "global-prefix": "^4.0.0", + "kdbush": "^4.0.2", + "murmurhash-js": "^1.0.0", + "pbf": "^3.3.0", + "potpack": "^2.0.0", + "quickselect": "^3.0.0", + "supercluster": "^8.0.1", + "tinyqueue": "^3.0.0", + "vt-pbf": "^3.1.3" + }, + "engines": { + "node": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" + } + }, + "node_modules/maplibre-gl/node_modules/@mapbox/tiny-sdf": { + "version": "2.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/tiny-sdf/-/tiny-sdf-2.1.0.tgz", + "integrity": "sha512-uFJhNh36BR4OCuWIEiWaEix9CA2WzT6CAIcqVjWYpnx8+QDtS+oC4QehRrx5cX4mgWs37MmKnwUejeHxVymzNg==", + "license": "BSD-2-Clause" + }, + "node_modules/maplibre-gl/node_modules/@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", + "license": "BSD-2-Clause" + }, + "node_modules/maplibre-gl/node_modules/earcut": { + "version": "3.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/earcut/-/earcut-3.0.2.tgz", + "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", + "license": "ISC" + }, + "node_modules/maplibre-gl/node_modules/geojson-vt": { + "version": "4.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/geojson-vt/-/geojson-vt-4.0.2.tgz", + "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", + "license": "ISC" + }, + "node_modules/maplibre-gl/node_modules/potpack": { + "version": "2.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/potpack/-/potpack-2.1.0.tgz", + "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", + "license": "ISC" + }, + "node_modules/maplibre-gl/node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, + "node_modules/maplibre-gl/node_modules/supercluster": { + "version": "8.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" + } + }, + "node_modules/maplibre-gl/node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, + "node_modules/math-log2": { + "version": "1.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/math-log2/-/math-log2-1.0.1.tgz", + "integrity": "sha512-9W0yGtkaMAkf74XGYVy4Dqw3YUMnTNB2eeiw9aQbUl4A3KmuCEHTt2DgAB07ENzOYAjsYSAYufkAq0Zd+jU7zA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/mdast-util-to-hast": { "version": "13.2.1", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", @@ -5386,12 +7093,59 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://npm-proxy.dev.databricks.com/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mouse-change": { + "version": "1.4.0", + "resolved": "https://npm-proxy.dev.databricks.com/mouse-change/-/mouse-change-1.4.0.tgz", + "integrity": "sha512-vpN0s+zLL2ykyyUDh+fayu9Xkor5v/zRD9jhSqjRS1cJTGS0+oakVZzNm5n19JvvEj0you+MXlYTpNxUDQUjkQ==", + "license": "MIT", + "dependencies": { + "mouse-event": "^1.0.0" + } + }, + "node_modules/mouse-event": { + "version": "1.0.5", + "resolved": "https://npm-proxy.dev.databricks.com/mouse-event/-/mouse-event-1.0.5.tgz", + "integrity": "sha512-ItUxtL2IkeSKSp9cyaX2JLUuKk2uMoxBg4bbOWVd29+CskYJR9BGsUqtXenNzKbnDshvupjUewDIYVrOB6NmGw==", + "license": "MIT" + }, + "node_modules/mouse-event-offset": { + "version": "3.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/mouse-event-offset/-/mouse-event-offset-3.0.2.tgz", + "integrity": "sha512-s9sqOs5B1Ykox3Xo8b3Ss2IQju4UwlW6LSR+Q5FXWpprJ5fzMLefIIItr3PH8RwzfGy6gxs/4GAmiNuZScE25w==", + "license": "MIT" + }, + "node_modules/mouse-wheel": { + "version": "1.2.0", + "resolved": "https://npm-proxy.dev.databricks.com/mouse-wheel/-/mouse-wheel-1.2.0.tgz", + "integrity": "sha512-+OfYBiUOCTWcTECES49neZwL5AoGkXE+lFjIvzwNCnYRlso+EnfvovcBxGoyQ0yQt806eSPjS675K0EwWknXmw==", + "license": "MIT", + "dependencies": { + "right-now": "^1.0.0", + "signum": "^1.0.0", + "to-px": "^1.0.1" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/murmurhash-js": { + "version": "1.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -5410,6 +7164,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/native-promise-only": { + "version": "0.8.1", + "resolved": "https://npm-proxy.dev.databricks.com/native-promise-only/-/native-promise-only-0.8.1.tgz", + "integrity": "sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==", + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5417,6 +7177,38 @@ "dev": true, "license": "MIT" }, + "node_modules/needle": { + "version": "2.9.1", + "resolved": "https://npm-proxy.dev.databricks.com/needle/-/needle-2.9.1.tgz", + "integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==", + "license": "MIT", + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://npm-proxy.dev.databricks.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "license": "ISC" + }, "node_modules/node-releases": { "version": "2.0.23", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", @@ -5442,6 +7234,42 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-svg-path": { + "version": "0.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/normalize-svg-path/-/normalize-svg-path-0.1.0.tgz", + "integrity": "sha512-1/kmYej2iedi5+ROxkRESL/pI02pkg0OBnaR4hJkSIX6+ORzepwbuUXfrdZaPjysTsJInj0Rj5NuX027+dMBvA==", + "license": "MIT" + }, + "node_modules/number-is-integer": { + "version": "1.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/number-is-integer/-/number-is-integer-1.0.1.tgz", + "integrity": "sha512-Dq3iuiFBkrbmuQjGFFF3zckXNCQoSD37/SdSbgcBailUx6knDvDwb5CympBgcoWHy36sfS12u74MHYkXyHq6bg==", + "license": "MIT", + "dependencies": { + "is-finite": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://npm-proxy.dev.databricks.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://npm-proxy.dev.databricks.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/oniguruma-parser": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", @@ -5524,6 +7352,33 @@ "node": ">=6" } }, + "node_modules/parenthesis": { + "version": "3.1.8", + "resolved": "https://npm-proxy.dev.databricks.com/parenthesis/-/parenthesis-3.1.8.tgz", + "integrity": "sha512-KF/U8tk54BgQewkJPvB4s/US3VQY68BRDpH638+7O/n58TpnwiwnOtGIOsT2/i+M78s61BBpeC83STB88d8sqw==", + "license": "MIT" + }, + "node_modules/parse-rect": { + "version": "1.2.0", + "resolved": "https://npm-proxy.dev.databricks.com/parse-rect/-/parse-rect-1.2.0.tgz", + "integrity": "sha512-4QZ6KYbnE6RTwg9E0HpLchUM9EZt6DnDxajFZZDSV4p/12ZJEvPO702DZpGvRYEPo00yKDys7jASi+/w7aO8LA==", + "license": "MIT", + "dependencies": { + "pick-by-alias": "^1.2.0" + } + }, + "node_modules/parse-svg-path": { + "version": "0.1.2", + "resolved": "https://npm-proxy.dev.databricks.com/parse-svg-path/-/parse-svg-path-0.1.2.tgz", + "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", + "license": "MIT" + }, + "node_modules/parse-unit": { + "version": "1.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/parse-unit/-/parse-unit-1.0.1.tgz", + "integrity": "sha512-hrqldJHokR3Qj88EIlV/kAyAi/G5R2+R56TBANxNMy0uPlYcttx0jnMW6Yx5KsKPSbC3KddM/7qQm3+0wEXKxg==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5544,12 +7399,43 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://npm-proxy.dev.databricks.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, + "node_modules/pbf": { + "version": "3.3.0", + "resolved": "https://npm-proxy.dev.databricks.com/pbf/-/pbf-3.3.0.tgz", + "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT" + }, + "node_modules/pick-by-alias": { + "version": "1.2.0", + "resolved": "https://npm-proxy.dev.databricks.com/pick-by-alias/-/pick-by-alias-1.2.0.tgz", + "integrity": "sha512-ESj2+eBxhGrcA1azgHs7lARG5+5iLakc/6nlfbpjcLl00HuuUOIuORhYXN4D1HfvMSKuVtFQjAlnwi1JHEeDIw==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5568,6 +7454,100 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/plotly.js": { + "version": "3.5.0", + "resolved": "https://npm-proxy.dev.databricks.com/plotly.js/-/plotly.js-3.5.0.tgz", + "integrity": "sha512-a3AYQIMG7OdZmrJ/fJ65HSt3g1l5qDeludKqjjafU1dh5E+fwqDhsEBndW7VCYwjlducCfN6KtPdWdiWFcoBWw==", + "license": "MIT", + "dependencies": { + "@plotly/d3": "3.8.2", + "@plotly/d3-sankey": "0.7.2", + "@plotly/d3-sankey-circular": "0.33.1", + "@plotly/mapbox-gl": "1.13.4", + "@plotly/regl": "^2.1.2", + "@turf/area": "^7.1.0", + "@turf/bbox": "^7.1.0", + "@turf/centroid": "^7.1.0", + "base64-arraybuffer": "^1.0.2", + "canvas-fit": "^1.5.0", + "color-alpha": "1.0.4", + "color-normalize": "1.5.0", + "color-parse": "2.0.0", + "color-rgba": "3.0.0", + "country-regex": "^1.1.0", + "d3-force": "^1.2.1", + "d3-format": "^1.4.5", + "d3-geo": "^1.12.1", + "d3-geo-projection": "^2.9.0", + "d3-hierarchy": "^1.1.9", + "d3-interpolate": "^3.0.1", + "d3-time": "^1.1.0", + "d3-time-format": "^2.2.3", + "fast-isnumeric": "^1.1.4", + "gl-mat4": "^1.2.0", + "gl-text": "^1.4.0", + "has-hover": "^1.0.1", + "has-passive-events": "^1.0.0", + "is-mobile": "^4.0.0", + "maplibre-gl": "^4.7.1", + "mouse-change": "^1.4.0", + "mouse-event-offset": "^3.0.2", + "mouse-wheel": "^1.2.0", + "native-promise-only": "^0.8.1", + "parse-svg-path": "^0.1.2", + "point-in-polygon": "^1.1.0", + "polybooljs": "^1.2.2", + "probe-image-size": "^7.2.3", + "regl-error2d": "^2.0.12", + "regl-line2d": "^3.1.3", + "regl-scatter2d": "^3.3.1", + "regl-splom": "^1.0.14", + "strongly-connected-components": "^1.0.1", + "superscript-text": "^1.0.0", + "svg-path-sdf": "^1.1.3", + "tinycolor2": "^1.4.2", + "to-px": "1.0.1", + "topojson-client": "^3.1.0", + "webgl-context": "^2.2.0", + "world-calendars": "^1.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/plotly.js/node_modules/d3-format": { + "version": "1.4.5", + "resolved": "https://npm-proxy.dev.databricks.com/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==", + "license": "BSD-3-Clause" + }, + "node_modules/plotly.js/node_modules/d3-time": { + "version": "1.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==", + "license": "BSD-3-Clause" + }, + "node_modules/plotly.js/node_modules/d3-time-format": { + "version": "2.3.0", + "resolved": "https://npm-proxy.dev.databricks.com/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-time": "1" + } + }, + "node_modules/point-in-polygon": { + "version": "1.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/point-in-polygon/-/point-in-polygon-1.1.0.tgz", + "integrity": "sha512-3ojrFwjnnw8Q9242TzgXuTD+eKiutbzyslcq1ydfu82Db2y+Ogbmyrkpv0Hgj31qwT3lbS9+QAAO/pIQM35XRw==", + "license": "MIT" + }, + "node_modules/polybooljs": { + "version": "1.2.2", + "resolved": "https://npm-proxy.dev.databricks.com/polybooljs/-/polybooljs-1.2.2.tgz", + "integrity": "sha512-ziHW/02J0XuNuUtmidBc6GXE8YohYydp3DWPWXYsd7O721TjcmN+k6ezjdwkDqep+gnWnFY+yqZHvzElra2oCg==", + "license": "MIT" + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -5603,6 +7583,12 @@ "dev": true, "license": "MIT" }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", + "license": "ISC" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5628,6 +7614,40 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/probe-image-size": { + "version": "7.2.3", + "resolved": "https://npm-proxy.dev.databricks.com/probe-image-size/-/probe-image-size-7.2.3.tgz", + "integrity": "sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w==", + "license": "MIT", + "dependencies": { + "lodash.merge": "^4.6.2", + "needle": "^2.5.2", + "stream-parser": "~0.3.1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://npm-proxy.dev.databricks.com/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://npm-proxy.dev.databricks.com/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", @@ -5639,6 +7659,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.1", + "resolved": "https://npm-proxy.dev.databricks.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz", + "integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5670,6 +7696,21 @@ ], "license": "MIT" }, + "node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", + "license": "ISC" + }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://npm-proxy.dev.databricks.com/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", @@ -5698,6 +7739,19 @@ "license": "MIT", "peer": true }, + "node_modules/react-plotly.js": { + "version": "2.6.0", + "resolved": "https://npm-proxy.dev.databricks.com/react-plotly.js/-/react-plotly.js-2.6.0.tgz", + "integrity": "sha512-g93xcyhAVCSt9kV1svqG1clAEdL6k3U+jjuSzfTV7owaSU9Go6Ph8bl25J+jKfKvIGAEYpe4qj++WHJuc9IaeA==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "plotly.js": ">1.34.0", + "react": ">0.13.0" + } + }, "node_modules/react-redux": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", @@ -5800,6 +7854,33 @@ } } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://npm-proxy.dev.databricks.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://npm-proxy.dev.databricks.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -5909,6 +7990,80 @@ "dev": true, "license": "MIT" }, + "node_modules/regl": { + "version": "2.1.1", + "resolved": "https://npm-proxy.dev.databricks.com/regl/-/regl-2.1.1.tgz", + "integrity": "sha512-+IOGrxl3FZ8ZM9ixCWQZzFRiRn7Rzn9bu3iFHwg/yz4tlOUQgbO4PHLgG+1ZT60zcIV8tief6Qrmyl8qcoJP0g==", + "license": "MIT" + }, + "node_modules/regl-error2d": { + "version": "2.0.12", + "resolved": "https://npm-proxy.dev.databricks.com/regl-error2d/-/regl-error2d-2.0.12.tgz", + "integrity": "sha512-r7BUprZoPO9AbyqM5qlJesrSRkl+hZnVKWKsVp7YhOl/3RIpi4UDGASGJY0puQ96u5fBYw/OlqV24IGcgJ0McA==", + "license": "MIT", + "dependencies": { + "array-bounds": "^1.0.1", + "color-normalize": "^1.5.0", + "flatten-vertex-data": "^1.0.2", + "object-assign": "^4.1.1", + "pick-by-alias": "^1.2.0", + "to-float32": "^1.1.0", + "update-diff": "^1.1.0" + } + }, + "node_modules/regl-line2d": { + "version": "3.1.3", + "resolved": "https://npm-proxy.dev.databricks.com/regl-line2d/-/regl-line2d-3.1.3.tgz", + "integrity": "sha512-fkgzW+tTn4QUQLpFKsUIE0sgWdCmXAM3ctXcCgoGBZTSX5FE2A0M7aynz7nrZT5baaftLrk9te54B+MEq4QcSA==", + "license": "MIT", + "dependencies": { + "array-bounds": "^1.0.1", + "array-find-index": "^1.0.2", + "array-normalize": "^1.1.4", + "color-normalize": "^1.5.0", + "earcut": "^2.1.5", + "es6-weak-map": "^2.0.3", + "flatten-vertex-data": "^1.0.2", + "object-assign": "^4.1.1", + "parse-rect": "^1.2.0", + "pick-by-alias": "^1.2.0", + "to-float32": "^1.1.0" + } + }, + "node_modules/regl-scatter2d": { + "version": "3.4.0", + "resolved": "https://npm-proxy.dev.databricks.com/regl-scatter2d/-/regl-scatter2d-3.4.0.tgz", + "integrity": "sha512-DavKQlHsI+iHZuLgOL+yGkg+sPd94CS+7FCBWkcQ6s/TbaNfUsF9eN591fjjSWIoKrGNfb/SEGhsXR5lXjqZ2w==", + "license": "MIT", + "dependencies": { + "@plotly/point-cluster": "^3.1.9", + "array-bounds": "^1.0.1", + "color-id": "^1.1.0", + "color-normalize": "^1.5.0", + "flatten-vertex-data": "^1.0.2", + "glslify": "^7.0.0", + "parse-rect": "^1.2.0", + "pick-by-alias": "^1.2.0", + "to-float32": "^1.1.0", + "update-diff": "^1.1.0" + } + }, + "node_modules/regl-splom": { + "version": "1.0.14", + "resolved": "https://npm-proxy.dev.databricks.com/regl-splom/-/regl-splom-1.0.14.tgz", + "integrity": "sha512-OiLqjmPRYbd7kDlHC6/zDf6L8lxgDC65BhC8JirhP4ykrK4x22ZyS+BnY8EUinXKDeMgmpRwCvUmk7BK4Nweuw==", + "license": "MIT", + "dependencies": { + "array-bounds": "^1.0.1", + "array-range": "^1.0.1", + "color-alpha": "^1.0.4", + "flatten-vertex-data": "^1.0.2", + "parse-rect": "^1.2.0", + "pick-by-alias": "^1.2.0", + "raf": "^3.4.1", + "regl-scatter2d": "^3.2.3" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5925,6 +8080,27 @@ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", "license": "MIT" }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://npm-proxy.dev.databricks.com/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -5944,6 +8120,15 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "license": "MIT", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -5955,6 +8140,12 @@ "node": ">=0.10.0" } }, + "node_modules/right-now": { + "version": "1.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/right-now/-/right-now-1.0.0.tgz", + "integrity": "sha512-DA8+YS+sMIVpbsuKgy+Z67L9Lxb1p05mNxRpDPNksPDEFir4vmBlUtuN9jkTGn9YMMdlBuK7XQgFiz6ws+yhSg==", + "license": "MIT" + }, "node_modules/rolldown": { "version": "1.0.0-beta.41", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.41.tgz", @@ -6018,6 +8209,47 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://npm-proxy.dev.databricks.com/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://npm-proxy.dev.databricks.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://npm-proxy.dev.databricks.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://npm-proxy.dev.databricks.com/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -6054,6 +8286,12 @@ "seroval": "^1.0" } }, + "node_modules/shallow-copy": { + "version": "0.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/shallow-copy/-/shallow-copy-0.0.1.tgz", + "integrity": "sha512-b6i4ZpVuUxB9h5gfCxPiusKYkqTMOjEbBs4wMaFbkfia4yFv92UKZ6Df8WXcKbn08JNL/abvg3FnMAOfakDvUw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -6094,6 +8332,12 @@ "@types/hast": "^3.0.4" } }, + "node_modules/signum": { + "version": "1.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/signum/-/signum-1.0.0.tgz", + "integrity": "sha512-yodFGwcyt59XRh7w5W3jPcIQb3Bwi21suEfT7MAWnBX3iCdklJpgDgvGT9o04UonglZN5SNMfJFkHIR/jO8GHw==", + "license": "MIT" + }, "node_modules/solid-js": { "version": "1.9.11", "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.11.tgz", @@ -6135,6 +8379,77 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/stack-trace": { + "version": "0.0.9", + "resolved": "https://npm-proxy.dev.databricks.com/stack-trace/-/stack-trace-0.0.9.tgz", + "integrity": "sha512-vjUc6sfgtgY0dxCdnc40mK6Oftjo9+2K8H/NG81TMhgL392FtiPA9tn9RLyTxXmTLPJPjF3VyzFp6bsWFLisMQ==", + "engines": { + "node": "*" + } + }, + "node_modules/static-eval": { + "version": "2.1.1", + "resolved": "https://npm-proxy.dev.databricks.com/static-eval/-/static-eval-2.1.1.tgz", + "integrity": "sha512-MgWpQ/ZjGieSVB3eOJVs4OA2LT/q1vx98KPCTTQPzq/aLr0YUXTsgryTXr4SLfR0ZfUUCiedM9n/ABeDIyy4mA==", + "license": "MIT", + "dependencies": { + "escodegen": "^2.1.0" + } + }, + "node_modules/stream-parser": { + "version": "0.3.1", + "resolved": "https://npm-proxy.dev.databricks.com/stream-parser/-/stream-parser-0.3.1.tgz", + "integrity": "sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==", + "license": "MIT", + "dependencies": { + "debug": "2" + } + }, + "node_modules/stream-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://npm-proxy.dev.databricks.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/stream-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://npm-proxy.dev.databricks.com/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://npm-proxy.dev.databricks.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://npm-proxy.dev.databricks.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/string-split-by": { + "version": "1.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/string-split-by/-/string-split-by-1.0.0.tgz", + "integrity": "sha512-KaJKY+hfpzNyet/emP81PJA9hTVSfxNLS9SFTWxdCnnW1/zOOwiV248+EfoX7IQFcBaOp4G5YE6xTJMF+pLg6A==", + "license": "MIT", + "dependencies": { + "parenthesis": "^3.1.5" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -6163,6 +8478,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strongly-connected-components": { + "version": "1.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/strongly-connected-components/-/strongly-connected-components-1.0.1.tgz", + "integrity": "sha512-i0TFx4wPcO0FwX+4RkLJi1MxmcTv90jNZgxMu9XRnMXMeFUY1VJlIoXpZunPUvUUqbCT1pg5PEkFqqpcaElNaA==", + "license": "MIT" + }, + "node_modules/supercluster": { + "version": "7.1.5", + "resolved": "https://npm-proxy.dev.databricks.com/supercluster/-/supercluster-7.1.5.tgz", + "integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==", + "license": "ISC", + "dependencies": { + "kdbush": "^3.0.0" + } + }, + "node_modules/supercluster/node_modules/kdbush": { + "version": "3.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/kdbush/-/kdbush-3.0.0.tgz", + "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==", + "license": "ISC" + }, + "node_modules/superscript-text": { + "version": "1.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/superscript-text/-/superscript-text-1.0.0.tgz", + "integrity": "sha512-gwu8l5MtRZ6koO0icVTlmN5pm7Dhh1+Xpe9O4x6ObMAsW+3jPbW14d1DsBq1F4wiI+WOFjXF35pslgec/G8yCQ==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6176,6 +8518,58 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://npm-proxy.dev.databricks.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-arc-to-cubic-bezier": { + "version": "3.2.0", + "resolved": "https://npm-proxy.dev.databricks.com/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", + "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==", + "license": "ISC" + }, + "node_modules/svg-path-bounds": { + "version": "1.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/svg-path-bounds/-/svg-path-bounds-1.0.2.tgz", + "integrity": "sha512-H4/uAgLWrppIC0kHsb2/dWUYSmb4GE5UqH06uqWBcg6LBjX2fu0A8+JrO2/FJPZiSsNOKZAhyFFgsLTdYUvSqQ==", + "license": "MIT", + "dependencies": { + "abs-svg-path": "^0.1.1", + "is-svg-path": "^1.0.1", + "normalize-svg-path": "^1.0.0", + "parse-svg-path": "^0.1.2" + } + }, + "node_modules/svg-path-bounds/node_modules/normalize-svg-path": { + "version": "1.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz", + "integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==", + "license": "MIT", + "dependencies": { + "svg-arc-to-cubic-bezier": "^3.0.0" + } + }, + "node_modules/svg-path-sdf": { + "version": "1.1.3", + "resolved": "https://npm-proxy.dev.databricks.com/svg-path-sdf/-/svg-path-sdf-1.1.3.tgz", + "integrity": "sha512-vJJjVq/R5lSr2KLfVXVAStktfcfa1pNFjFOgyJnzZFXlO/fDZ5DmM8FpnSKKzLPfEYTVeXuVBTHF296TpxuJVg==", + "license": "MIT", + "dependencies": { + "bitmap-sdf": "^1.0.0", + "draw-svg-path": "^1.0.0", + "is-svg-path": "^1.0.1", + "parse-svg-path": "^0.1.2", + "svg-path-bounds": "^1.0.1" + } + }, "node_modules/tailwind-merge": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", @@ -6215,6 +8609,16 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://npm-proxy.dev.databricks.com/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -6227,6 +8631,12 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", "license": "MIT" }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://npm-proxy.dev.databricks.com/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -6272,6 +8682,27 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyqueue": { + "version": "2.0.3", + "resolved": "https://npm-proxy.dev.databricks.com/tinyqueue/-/tinyqueue-2.0.3.tgz", + "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==", + "license": "ISC" + }, + "node_modules/to-float32": { + "version": "1.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/to-float32/-/to-float32-1.1.0.tgz", + "integrity": "sha512-keDnAusn/vc+R3iEiSDw8TOF7gPiTLdK1ArvWtYbJQiVfmRg6i/CAvbKq3uIS0vWroAC7ZecN3DjQKw3aSklUg==", + "license": "MIT" + }, + "node_modules/to-px": { + "version": "1.0.1", + "resolved": "https://npm-proxy.dev.databricks.com/to-px/-/to-px-1.0.1.tgz", + "integrity": "sha512-2y3LjBeIZYL19e5gczp14/uRWFDtDUErJPVN3VU9a7SJO+RjGRtYR47aMN2bZgGlxvW4ZcEz2ddUPVHXcMfuXw==", + "license": "MIT", + "dependencies": { + "parse-unit": "^1.0.1" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6284,6 +8715,20 @@ "node": ">=8.0" } }, + "node_modules/topojson-client": { + "version": "3.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", + "license": "ISC", + "dependencies": { + "commander": "2" + }, + "bin": { + "topo2geo": "bin/topo2geo", + "topomerge": "bin/topomerge", + "topoquantize": "bin/topoquantize" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -6342,6 +8787,12 @@ "url": "https://github.com/sponsors/Wombosvideo" } }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://npm-proxy.dev.databricks.com/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "license": "ISC" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6355,6 +8806,22 @@ "node": ">= 0.8.0" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://npm-proxy.dev.databricks.com/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typedarray-pool": { + "version": "1.2.0", + "resolved": "https://npm-proxy.dev.databricks.com/typedarray-pool/-/typedarray-pool-1.2.0.tgz", + "integrity": "sha512-YTSQbzX43yvtpfRtIDAYygoYtgT+Rpjuxy9iOpczrjpXLgGoyG7aS5USJXV2d3nn8uHTeb9rXDvzS27zUg5KYQ==", + "license": "MIT", + "dependencies": { + "bit-twiddle": "^1.0.0", + "dup": "^1.0.0" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -6500,6 +8967,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/unquote": { + "version": "1.1.1", + "resolved": "https://npm-proxy.dev.databricks.com/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==", + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -6530,6 +9003,12 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/update-diff": { + "version": "1.1.0", + "resolved": "https://npm-proxy.dev.databricks.com/update-diff/-/update-diff-1.1.0.tgz", + "integrity": "sha512-rCiBPiHxZwT4+sBhEbChzpO5hYHjm91kScWgdHf4Qeafs6Ba7MBl+d9GlGv72bcTZQO0sLmtQS1pHSWoCLtN/A==", + "license": "MIT" + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -6592,6 +9071,12 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -6749,6 +9234,32 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vt-pbf": { + "version": "3.1.3", + "resolved": "https://npm-proxy.dev.databricks.com/vt-pbf/-/vt-pbf-3.1.3.tgz", + "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", + "license": "MIT", + "dependencies": { + "@mapbox/point-geometry": "0.1.0", + "@mapbox/vector-tile": "^1.3.1", + "pbf": "^3.2.1" + } + }, + "node_modules/weak-map": { + "version": "1.0.8", + "resolved": "https://npm-proxy.dev.databricks.com/weak-map/-/weak-map-1.0.8.tgz", + "integrity": "sha512-lNR9aAefbGPpHO7AEnY0hCFjz1eTkWCXYvkTRrTHs9qv8zJp+SkVYpzfLIFXQQiG3tVvbNFQgVg2bQS8YGgxyw==", + "license": "Apache-2.0" + }, + "node_modules/webgl-context": { + "version": "2.2.0", + "resolved": "https://npm-proxy.dev.databricks.com/webgl-context/-/webgl-context-2.2.0.tgz", + "integrity": "sha512-q/fGIivtqTT7PEoF07axFIlHNk/XCPaYpq64btnepopSWvKNFkoORlQYgqDigBIuGA1ExnFd/GnSUnBNEPQY7Q==", + "license": "MIT", + "dependencies": { + "get-canvas-context": "^1.0.1" + } + }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", @@ -6781,6 +9292,30 @@ "node": ">=0.10.0" } }, + "node_modules/world-calendars": { + "version": "1.0.4", + "resolved": "https://npm-proxy.dev.databricks.com/world-calendars/-/world-calendars-1.0.4.tgz", + "integrity": "sha512-VGRnLJS+xJmGDPodgJRnGIDwGu0s+Cr9V2HB3EzlDZ5n0qb8h5SJtGUEkjrphZYAglEiXZ6kiXdmk0H/h/uu/w==", + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://npm-proxy.dev.databricks.com/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/apps/dev-playground/client/src/routes/metrics.route.tsx b/apps/dev-playground/client/src/routes/metrics.route.tsx index 2455ae340..6154549e8 100644 --- a/apps/dev-playground/client/src/routes/metrics.route.tsx +++ b/apps/dev-playground/client/src/routes/metrics.route.tsx @@ -167,8 +167,12 @@ function RevenueChart() { // Wire metadata into Plotly layout. `formatLabel` returns the YAML-defined // display name; `toD3Format` converts the YAML's printf-style format spec // into the d3-format syntax that Plotly's `tickformat` understands. + // Defensive: `metric sync` may have emitted an empty measures/dimensions + // record (e.g., DESCRIBE response shape mismatch for joined metric views). + // formatLabel + toD3Format both fall back gracefully when given undefined, + // so this stays safe even when the metadata bundle is incomplete. const arrLabel = formatLabel("arr", metadata?.measures.arr); - const arrTickFormat = toD3Format(metadata?.measures.arr.format); + const arrTickFormat = toD3Format(metadata?.measures.arr?.format); return ( {formatValue( row.active_accounts, - metadata?.measures.active_accounts.format, + metadata?.measures.active_accounts?.format, )} diff --git a/apps/dev-playground/shared/appkit-types/metric.d.ts b/apps/dev-playground/shared/appkit-types/metric.d.ts index d07e5d839..1cda6dc01 100644 --- a/apps/dev-playground/shared/appkit-types/metric.d.ts +++ b/apps/dev-playground/shared/appkit-types/metric.d.ts @@ -7,62 +7,14 @@ declare module "@databricks/appkit-ui/react" { key: "revenue"; source: "appkit_demo.public.revenue_metrics"; lane: "sp"; - measures: { - /** @sqlType double */ - "mrr": number; - /** @sqlType double */ - "arr": number; - /** @sqlType double */ - "new_arr": number; - /** @sqlType double */ - "churned_arr": number; - }; - dimensions: { - /** @sqlType string */ - "region": string; - /** @sqlType string */ - "segment": string; - /** @sqlType timestamp_ltz @timeGrain day|hour|minute|month|quarter|week|year */ - "created_at": string; - }; - measureKeys: "mrr" | "arr" | "new_arr" | "churned_arr"; - dimensionKeys: "region" | "segment" | "created_at"; - timeGrains: "day" | "hour" | "minute" | "month" | "quarter" | "week" | "year"; + measures: Record; + dimensions: Record; + measureKeys: never; + dimensionKeys: never; + timeGrains: never; metadata: { - measures: { - "mrr": { - type: "double"; - display_name: "Monthly Recurring Revenue"; - }; - "arr": { - type: "double"; - display_name: "Annual Recurring Revenue"; - description: "Annualized contract value across all active subscriptions"; - }; - "new_arr": { - type: "double"; - display_name: "New ARR"; - }; - "churned_arr": { - type: "double"; - display_name: "Churned ARR"; - }; - }; - dimensions: { - "region": { - type: "string"; - display_name: "Region"; - }; - "segment": { - type: "string"; - display_name: "Customer Segment"; - }; - "created_at": { - type: "timestamp_ltz"; - display_name: "Subscription Start"; - time_grain: readonly ["day", "hour", "minute", "month", "quarter", "week", "year"]; - }; - }; + measures: Record; + dimensions: Record; }; }; "customer_metrics": { diff --git a/apps/dev-playground/shared/appkit-types/metrics.metadata.json b/apps/dev-playground/shared/appkit-types/metrics.metadata.json index 338133624..2933a738e 100644 --- a/apps/dev-playground/shared/appkit-types/metrics.metadata.json +++ b/apps/dev-playground/shared/appkit-types/metrics.metadata.json @@ -34,47 +34,7 @@ "revenue": { "source": "appkit_demo.public.revenue_metrics", "lane": "sp", - "measures": { - "mrr": { - "type": "double", - "display_name": "Monthly Recurring Revenue" - }, - "arr": { - "type": "double", - "display_name": "Annual Recurring Revenue", - "description": "Annualized contract value across all active subscriptions" - }, - "new_arr": { - "type": "double", - "display_name": "New ARR" - }, - "churned_arr": { - "type": "double", - "display_name": "Churned ARR" - } - }, - "dimensions": { - "region": { - "type": "string", - "display_name": "Region" - }, - "segment": { - "type": "string", - "display_name": "Customer Segment" - }, - "created_at": { - "type": "timestamp_ltz", - "display_name": "Subscription Start", - "time_grain": [ - "day", - "hour", - "minute", - "month", - "quarter", - "week", - "year" - ] - } - } + "measures": {}, + "dimensions": {} } } diff --git a/packages/appkit/src/type-generator/metric-registry.ts b/packages/appkit/src/type-generator/metric-registry.ts index f9bf81b9b..dcfaa91be 100644 --- a/packages/appkit/src/type-generator/metric-registry.ts +++ b/packages/appkit/src/type-generator/metric-registry.ts @@ -852,6 +852,19 @@ export async function syncMetrics( const measures = columns.filter((c) => c.isMeasure); const dimensions = columns.filter((c) => !c.isMeasure); + // Warn when extraction succeeded but yielded no columns. The most common + // cause is a DESCRIBE response shape that `extractMetricColumns` doesn't + // recognize (e.g., joined metric views may put columns under a wrapper + // that the v0.1 reader doesn't traverse). Without this signal, the bundle + // ships with empty measures/dimensions and downstream consumers blow up + // on `metadata.measures..format` accesses. + if (columns.length === 0) { + logger.warn( + "DESCRIBE response for %s yielded zero columns — the bundle entry will have empty measures/dimensions. Check that the response shape has a top-level `columns` array (or `schema.fields`).", + entry.source, + ); + } + schemas.push({ key: entry.key, source: entry.source, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c301f37d..947f00758 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -167,124 +167,6 @@ importers: specifier: npm:rolldown-vite@7.1.14 version: rolldown-vite@7.1.14(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2) - apps/dev-playground/client: - dependencies: - '@radix-ui/react-dropdown-menu': - specifier: 2.1.16 - version: 2.1.16(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-select': - specifier: 2.2.6 - version: 2.2.6(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-slot': - specifier: 1.2.3 - version: 1.2.3(@types/react@19.2.2)(react@19.2.0) - '@radix-ui/react-tooltip': - specifier: 1.2.8 - version: 1.2.8(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@tanstack/react-router': - specifier: 1.133.22 - version: 1.133.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@tanstack/react-router-devtools': - specifier: 1.133.22 - version: 1.133.22(@tanstack/react-router@1.133.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.133.20)(@types/node@24.6.0)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.12)(terser@5.44.1)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.2) - '@tanstack/react-table': - specifier: 8.21.3 - version: 8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@tanstack/router-plugin': - specifier: 1.133.22 - version: 1.133.22(@tanstack/react-router@1.133.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(rolldown-vite@7.1.14(@types/node@24.6.0)(esbuild@0.25.10)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2))(webpack@5.103.0(esbuild@0.25.10)) - class-variance-authority: - specifier: 0.7.1 - version: 0.7.1 - clsx: - specifier: 2.1.1 - version: 2.1.1 - lucide-react: - specifier: 0.546.0 - version: 0.546.0(react@19.2.0) - plotly.js: - specifier: ^3.5.0 - version: 3.5.0(mapbox-gl@1.13.3) - react: - specifier: 19.2.0 - version: 19.2.0 - react-dom: - specifier: 19.2.0 - version: 19.2.0(react@19.2.0) - react-plotly.js: - specifier: ^2.6.0 - version: 2.6.0(plotly.js@3.5.0(mapbox-gl@1.13.3))(react@19.2.0) - recharts: - specifier: 3.4.1 - version: 3.4.1(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react-is@18.3.1)(react@19.2.0)(redux@5.0.1) - tailwind-merge: - specifier: 3.3.1 - version: 3.3.1 - tailwindcss-animate: - specifier: 1.0.7 - version: 1.0.7(tailwindcss@4.1.17) - tw-animate-css: - specifier: 1.4.0 - version: 1.4.0 - devDependencies: - '@eslint/js': - specifier: 9.36.0 - version: 9.36.0 - '@tailwindcss/postcss': - specifier: 4.1.17 - version: 4.1.17 - '@tanstack/router-cli': - specifier: 1.133.20 - version: 1.133.20 - '@types/node': - specifier: 24.6.0 - version: 24.6.0 - '@types/react': - specifier: 19.2.2 - version: 19.2.2 - '@types/react-dom': - specifier: 19.2.2 - version: 19.2.2(@types/react@19.2.2) - '@types/react-plotly.js': - specifier: ^2.6.4 - version: 2.6.4 - '@vitejs/plugin-react': - specifier: 5.0.4 - version: 5.0.4(rolldown-vite@7.1.14(@types/node@24.6.0)(esbuild@0.25.10)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2)) - autoprefixer: - specifier: 10.4.21 - version: 10.4.21(postcss@8.5.6) - eslint: - specifier: 9.36.0 - version: 9.36.0(jiti@2.6.1) - eslint-plugin-react-hooks: - specifier: 5.2.0 - version: 5.2.0(eslint@9.36.0(jiti@2.6.1)) - eslint-plugin-react-refresh: - specifier: 0.4.22 - version: 0.4.22(eslint@9.36.0(jiti@2.6.1)) - globals: - specifier: 16.4.0 - version: 16.4.0 - postcss: - specifier: 8.5.6 - version: 8.5.6 - shiki: - specifier: 3.15.0 - version: 3.15.0 - tailwindcss: - specifier: 4.1.17 - version: 4.1.17 - typescript: - specifier: 5.9.3 - version: 5.9.3 - typescript-eslint: - specifier: 8.45.0 - version: 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) - vite: - specifier: npm:rolldown-vite@7.1.14 - version: rolldown-vite@7.1.14(@types/node@24.6.0)(esbuild@0.25.10)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2) - docs: dependencies: '@databricks/appkit-ui': @@ -1659,10 +1541,6 @@ packages: '@chevrotain/utils@11.0.3': resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} - '@choojs/findup@0.2.1': - resolution: {integrity: sha512-YstAqNb0MCN8PjdLCDfRsBcGVRN41f3vgLvaI0IrIcBp4AqILRSS0DeWNGkicC+f/zRIPJLc+9RURVSepwvfBw==} - hasBin: true - '@clack/core@1.0.1': resolution: {integrity: sha512-WKeyK3NOBwDOzagPR5H08rFk9D/WuN705yEbuZvKqlkmoLM2woKtXb10OO2k1NoSU4SFG947i2/SCYh+2u5e4g==} @@ -2458,18 +2336,10 @@ packages: resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.3.1': - resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.4.2': resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.15.2': - resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.17.0': resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2478,10 +2348,6 @@ packages: resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.36.0': - resolution: {integrity: sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.39.1': resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2490,10 +2356,6 @@ packages: resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.3.5': - resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.4.1': resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2787,48 +2649,6 @@ packages: '@leichtgewicht/ip-codec@2.0.5': resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} - '@mapbox/geojson-rewind@0.5.2': - resolution: {integrity: sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==} - hasBin: true - - '@mapbox/geojson-types@1.0.2': - resolution: {integrity: sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw==} - - '@mapbox/jsonlint-lines-primitives@2.0.2': - resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==} - engines: {node: '>= 0.6'} - - '@mapbox/mapbox-gl-supported@1.5.0': - resolution: {integrity: sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==} - peerDependencies: - mapbox-gl: '>=0.32.1 <2.0.0' - - '@mapbox/point-geometry@0.1.0': - resolution: {integrity: sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==} - - '@mapbox/tiny-sdf@1.2.5': - resolution: {integrity: sha512-cD8A/zJlm6fdJOk6DqPUV8mcpyJkRz2x2R+/fYcWDYG3oWbG7/L7Yl/WqQ1VZCjnL9OTIMAn6c+BC5Eru4sQEw==} - - '@mapbox/tiny-sdf@2.1.0': - resolution: {integrity: sha512-uFJhNh36BR4OCuWIEiWaEix9CA2WzT6CAIcqVjWYpnx8+QDtS+oC4QehRrx5cX4mgWs37MmKnwUejeHxVymzNg==} - - '@mapbox/unitbezier@0.0.0': - resolution: {integrity: sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==} - - '@mapbox/unitbezier@0.0.1': - resolution: {integrity: sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==} - - '@mapbox/vector-tile@1.3.1': - resolution: {integrity: sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==} - - '@mapbox/whoots-js@3.1.0': - resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==} - engines: {node: '>=6.0.0'} - - '@maplibre/maplibre-gl-style-spec@20.4.0': - resolution: {integrity: sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==} - hasBin: true - '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} @@ -3556,25 +3376,6 @@ packages: engines: {node: '>=18'} hasBin: true - '@plotly/d3-sankey-circular@0.33.1': - resolution: {integrity: sha512-FgBV1HEvCr3DV7RHhDsPXyryknucxtfnLwPtCKKxdolKyTFYoLX/ibEfX39iFYIL7DYbVeRtP43dbFcrHNE+KQ==} - - '@plotly/d3-sankey@0.7.2': - resolution: {integrity: sha512-2jdVos1N3mMp3QW0k2q1ph7Gd6j5PY1YihBrwpkFnKqO+cqtZq3AdEYUeSGXMeLsBDQYiqTVcihYfk8vr5tqhw==} - - '@plotly/d3@3.8.2': - resolution: {integrity: sha512-wvsNmh1GYjyJfyEBPKJLTMzgf2c2bEbSIL50lmqVUi+o1NHaLPi1Lb4v7VxXXJn043BhNyrxUrWI85Q+zmjOVA==} - - '@plotly/mapbox-gl@1.13.4': - resolution: {integrity: sha512-sR3/Pe5LqT/fhYgp4rT4aSFf1rTsxMbGiH6Hojc7PH36ny5Bn17iVFUjpzycafETURuFbLZUfjODO8LvSI+5zQ==} - engines: {node: '>=6.4.0'} - - '@plotly/point-cluster@3.1.9': - resolution: {integrity: sha512-MwaI6g9scKf68Orpr1pHZ597pYx9uP8UEFXLPbsCmuw3a84obwz6pnMXGc90VhgDNeNiLEdlmuK7CPo+5PIxXw==} - - '@plotly/regl@2.1.2': - resolution: {integrity: sha512-Mdk+vUACbQvjd0m/1JJjOOafmkp/EpmHjISsopEz5Av44CBq7rPC05HHNbYGKVyNUF2zmEoBS/TT0pd0SPFFyw==} - '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -4270,17 +4071,6 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} - '@reduxjs/toolkit@2.11.2': - resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} - peerDependencies: - react: ^16.9.0 || ^17.0.0 || ^18 || ^19 - react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 - peerDependenciesMeta: - react: - optional: true - react-redux: - optional: true - '@release-it/conventional-changelog@10.0.4': resolution: {integrity: sha512-pU1JkAZBHVk9u0O9CZcaLsqSZHWu0s9WNIFVUq0M9r/WlLpJvrCiSH2OCLo5XyOnWacdMvBjijm+kl6m36SdrA==} engines: {node: ^20.12.0 || >=22.0.0} @@ -4675,33 +4465,15 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@shikijs/core@3.15.0': - resolution: {integrity: sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg==} - - '@shikijs/engine-javascript@3.15.0': - resolution: {integrity: sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg==} - - '@shikijs/engine-oniguruma@3.15.0': - resolution: {integrity: sha512-HnqFsV11skAHvOArMZdLBZZApRSYS4LSztk2K3016Y9VCyZISnlYUYsL2hzlS7tPqKHvNqmI5JSUJZprXloMvA==} - '@shikijs/engine-oniguruma@3.20.0': resolution: {integrity: sha512-Yx3gy7xLzM0ZOjqoxciHjA7dAt5tyzJE3L4uQoM83agahy+PlW244XJSrmJRSBvGYELDhYXPacD4R/cauV5bzQ==} - '@shikijs/langs@3.15.0': - resolution: {integrity: sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A==} - '@shikijs/langs@3.20.0': resolution: {integrity: sha512-le+bssCxcSHrygCWuOrYJHvjus6zhQ2K7q/0mgjiffRbkhM4o1EWu2m+29l0yEsHDbWaWPNnDUTRVVBvBBeKaA==} - '@shikijs/themes@3.15.0': - resolution: {integrity: sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ==} - '@shikijs/themes@3.20.0': resolution: {integrity: sha512-U1NSU7Sl26Q7ErRvJUouArxfM2euWqq1xaSrbqMu2iqa+tSp0D1Yah8216sDYbdDHw4C8b75UpE65eWorm2erQ==} - '@shikijs/types@3.15.0': - resolution: {integrity: sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw==} - '@shikijs/types@3.20.0': resolution: {integrity: sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw==} @@ -4761,9 +4533,6 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@standard-schema/utils@0.3.0': - resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} - '@svgr/babel-plugin-add-jsx-attribute@8.0.0': resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} engines: {node: '>=14'} @@ -4849,79 +4618,39 @@ packages: resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} - '@tailwindcss/node@4.1.17': - resolution: {integrity: sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==} - '@tailwindcss/node@4.1.18': resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} - '@tailwindcss/oxide-android-arm64@4.1.17': - resolution: {integrity: sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [android] - '@tailwindcss/oxide-android-arm64@4.1.18': resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.1.17': - resolution: {integrity: sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - '@tailwindcss/oxide-darwin-arm64@4.1.18': resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.1.17': - resolution: {integrity: sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.1.18': resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.1.17': - resolution: {integrity: sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==} - engines: {node: '>= 10'} - cpu: [x64] - os: [freebsd] - '@tailwindcss/oxide-freebsd-x64@4.1.18': resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': - resolution: {integrity: sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': - resolution: {integrity: sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [glibc] - '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} engines: {node: '>= 10'} @@ -4929,13 +4658,6 @@ packages: os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-arm64-musl@4.1.17': - resolution: {integrity: sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [musl] - '@tailwindcss/oxide-linux-arm64-musl@4.1.18': resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} @@ -4943,13 +4665,6 @@ packages: os: [linux] libc: [musl] - '@tailwindcss/oxide-linux-x64-gnu@4.1.17': - resolution: {integrity: sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [glibc] - '@tailwindcss/oxide-linux-x64-gnu@4.1.18': resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} @@ -4957,13 +4672,6 @@ packages: os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-x64-musl@4.1.17': - resolution: {integrity: sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [musl] - '@tailwindcss/oxide-linux-x64-musl@4.1.18': resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} @@ -4971,18 +4679,6 @@ packages: os: [linux] libc: [musl] - '@tailwindcss/oxide-wasm32-wasi@4.1.17': - resolution: {integrity: sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - bundledDependencies: - - '@napi-rs/wasm-runtime' - - '@emnapi/core' - - '@emnapi/runtime' - - '@tybys/wasm-util' - - '@emnapi/wasi-threads' - - tslib - '@tailwindcss/oxide-wasm32-wasi@4.1.18': resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} engines: {node: '>=14.0.0'} @@ -4995,69 +4691,25 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': - resolution: {integrity: sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.1.17': - resolution: {integrity: sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.1.18': resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.1.17': - resolution: {integrity: sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==} - engines: {node: '>= 10'} - '@tailwindcss/oxide@4.1.18': resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} engines: {node: '>= 10'} - '@tailwindcss/postcss@4.1.17': - resolution: {integrity: sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==} - '@tailwindcss/postcss@4.1.18': resolution: {integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==} - '@tanstack/history@1.133.19': - resolution: {integrity: sha512-Y866qBVVprdQkmO0/W1AFBI8tiQy398vFeIwP+VrRWCOzs3VecxSVzAvaOM4iHfkJz81fFAZMhLLjDVoPikD+w==} - engines: {node: '>=12'} - - '@tanstack/react-router-devtools@1.133.22': - resolution: {integrity: sha512-YG498dyttY7yszEGo0iE4S3ymNrX+PSWXbP7zy94RhLf3mizupInxlKaypxhIU16toKiyOQzgFgOqi6v4RqfEQ==} - engines: {node: '>=12'} - peerDependencies: - '@tanstack/react-router': ^1.133.22 - react: '>=18.0.0 || >=19.0.0' - react-dom: '>=18.0.0 || >=19.0.0' - - '@tanstack/react-router@1.133.22': - resolution: {integrity: sha512-0tg2yoXVMvvgR3UdOhEX9ICmgZ/Ou/I8VOl07exSYEJYfyCr5nhtB/62F9NGbuUZVrJnCzc8Rz0e4/MYU18pIg==} - engines: {node: '>=12'} - peerDependencies: - react: '>=18.0.0 || >=19.0.0' - react-dom: '>=18.0.0 || >=19.0.0' - - '@tanstack/react-store@0.7.7': - resolution: {integrity: sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/react-table@8.21.3': resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} engines: {node: '>=12'} @@ -5065,67 +4717,10 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/router-cli@1.133.20': - resolution: {integrity: sha512-XFghXTGUDzBhLbe5UWikLDbcAcuDfqWtlJvyVhDl7rYV7Pvkdb8hGgbxsriUpaVKPx5nmud8JGINIW56lQUTyA==} - engines: {node: '>=12'} - hasBin: true - - '@tanstack/router-core@1.133.20': - resolution: {integrity: sha512-cO8E6XA0vMX2BaPZck9kfgXK76e6Lqo13GmXEYxtXshmW8cIlgcLHhBDKnI/sCjIy9OPY2sV1qrGHtcxJy/4ew==} - engines: {node: '>=12'} - - '@tanstack/router-devtools-core@1.133.22': - resolution: {integrity: sha512-Pcpyrd3rlNA6C1jnL6jy4pC/8s4PN7270RM7+krnlKex1Rk3REgQ5LXAaAJJxOXS2coY14tiQtfQS3gx+H3b4w==} - engines: {node: '>=12'} - peerDependencies: - '@tanstack/router-core': ^1.133.20 - csstype: ^3.0.10 - solid-js: '>=1.9.5' - tiny-invariant: ^1.3.3 - peerDependenciesMeta: - csstype: - optional: true - - '@tanstack/router-generator@1.133.20': - resolution: {integrity: sha512-63lhmNNoVfqTgnSx5MUnEl/QBKSN6hA1sWLhZSQhCjLp9lrWbCXM8l9QpG3Tgzq/LdX7jjDMf783sUL4p4NbYw==} - engines: {node: '>=12'} - - '@tanstack/router-plugin@1.133.22': - resolution: {integrity: sha512-VVUazrxqFyon9bFSFY2mysgTbQAH5BV8kP8Gq1IHd7AxlboRW9tnj6TQcy8KGgG/KPCbKB9CFZtvSheKqrAVQg==} - engines: {node: '>=12'} - peerDependencies: - '@rsbuild/core': '>=1.0.2' - '@tanstack/react-router': ^1.133.22 - vite: '>=5.0.0 || >=6.0.0 || >=7.0.0' - vite-plugin-solid: ^2.11.10 - webpack: '>=5.92.0' - peerDependenciesMeta: - '@rsbuild/core': - optional: true - '@tanstack/react-router': - optional: true - vite: - optional: true - vite-plugin-solid: - optional: true - webpack: - optional: true - - '@tanstack/router-utils@1.133.19': - resolution: {integrity: sha512-WEp5D2gPxvlLDRXwD/fV7RXjYtqaqJNXKB/L6OyZEbT+9BG/Ib2d7oG9GSUZNNMGPGYAlhBUOi3xutySsk6rxA==} - engines: {node: '>=12'} - - '@tanstack/store@0.7.7': - resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==} - '@tanstack/table-core@8.21.3': resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} - '@tanstack/virtual-file-routes@1.133.19': - resolution: {integrity: sha512-IKwZENsK7owmW1Lm5FhuHegY/SyQ8KqtL/7mTSnzoKJgfzhrrf9qwKB1rmkKkt+svUuy/Zw3uVEpZtUzQruWtA==} - engines: {node: '>=12'} - '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} @@ -5152,21 +4747,6 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} - '@turf/area@7.3.5': - resolution: {integrity: sha512-sSn80wPT7XfBIDN3vurCPxhk9W4U8ozS/XImSqeLN8qveTICOxzZkhsGDMp0CuncaN+plWut4a2TdNM7mzZB6Q==} - - '@turf/bbox@7.3.5': - resolution: {integrity: sha512-oG1ya/HtBjAIg4TimbWx+nOYPbY0bCvt82Bq8tm6sBw3qqtbOyRSfDz79Sq90TnH7DXJprJ1qnVGKNtZ6jemfw==} - - '@turf/centroid@7.3.5': - resolution: {integrity: sha512-hkWaqwGFdOn6Tf0EWfn2yn1XZ1FWE1h2C5ZWstDMu/FxYO5DB+YjlmOFPl4K6SmSOEgdV07eK2vDCyPeTHqKGA==} - - '@turf/helpers@7.3.5': - resolution: {integrity: sha512-E/NMGV5MwbjjP7AJXBtsanC3yY8N2MQ87IGdIgkB2ji5AtBpwnH4L3gEqpYN4RlCJJWbLbzO91BbKv2waUd0eg==} - - '@turf/meta@7.3.5': - resolution: {integrity: sha512-r+ohqxoyqeigFB0oFrQx/YEHIkOKqcKpCjvZkvZs7Tkv+IFco5MezAd2zd4rzK+0DfFgDP3KpJc7HqrYjvEjhg==} - '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -5335,9 +4915,6 @@ packages: '@types/express@4.17.25': resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} - '@types/geojson-vt@3.2.5': - resolution: {integrity: sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==} - '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} @@ -5383,12 +4960,6 @@ packages: '@types/lodash@4.17.24': resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} - '@types/mapbox__point-geometry@0.1.4': - resolution: {integrity: sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==} - - '@types/mapbox__vector-tile@1.3.4': - resolution: {integrity: sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==} - '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -5444,9 +5015,6 @@ packages: '@types/parse5@5.0.3': resolution: {integrity: sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==} - '@types/pbf@3.0.5': - resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==} - '@types/pg-pool@2.0.6': resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} @@ -5456,9 +5024,6 @@ packages: '@types/pg@8.16.0': resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==} - '@types/plotly.js@3.0.10': - resolution: {integrity: sha512-q+MgO4aajC2HrO7FllTYWzrpdfbTjboSMfjkz/aXKjg1v7HNo1zMEFfAW7quKfk6SL+bH74A5ThBEps/7hZxOA==} - '@types/prismjs@1.26.5': resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} @@ -5478,9 +5043,6 @@ packages: peerDependencies: '@types/react': ^19.2.0 - '@types/react-plotly.js@2.6.4': - resolution: {integrity: sha512-AU6w1u3qEGM0NmBA69PaOgNc0KPFA/+qkH6Uu9EBTJ45/WYOUoXi9AF5O15PRM2klpHSiHAAs4WnlI+OZAFmUA==} - '@types/react-router-config@5.0.11': resolution: {integrity: sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw==} @@ -5520,9 +5082,6 @@ packages: '@types/sockjs@0.3.36': resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==} - '@types/supercluster@7.1.3': - resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==} - '@types/tedious@4.0.14': resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} @@ -5535,9 +5094,6 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@types/use-sync-external-store@0.0.6': - resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} - '@types/validator@13.15.10': resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} @@ -5550,14 +5106,6 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.45.0': - resolution: {integrity: sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.45.0 - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/eslint-plugin@8.49.0': resolution: {integrity: sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5566,13 +5114,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.45.0': - resolution: {integrity: sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.49.0': resolution: {integrity: sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5580,45 +5121,22 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.45.0': - resolution: {integrity: sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.49.0': resolution: {integrity: sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.45.0': - resolution: {integrity: sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.49.0': resolution: {integrity: sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.45.0': - resolution: {integrity: sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/tsconfig-utils@8.49.0': resolution: {integrity: sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.45.0': - resolution: {integrity: sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.49.0': resolution: {integrity: sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5626,33 +5144,16 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.45.0': - resolution: {integrity: sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.49.0': resolution: {integrity: sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.45.0': - resolution: {integrity: sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA==} + '@typescript-eslint/typescript-estree@8.49.0': + resolution: {integrity: sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/typescript-estree@8.49.0': - resolution: {integrity: sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/utils@8.45.0': - resolution: {integrity: sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.49.0': resolution: {integrity: sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5660,10 +5161,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.45.0': - resolution: {integrity: sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.49.0': resolution: {integrity: sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5783,9 +5280,6 @@ packages: resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==} engines: {node: ^20.17.0 || >=22.9.0} - abs-svg-path@0.1.1: - resolution: {integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==} - accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -5810,11 +5304,6 @@ packages: resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} engines: {node: '>=0.4.0'} - acorn@7.4.1: - resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -5957,25 +5446,12 @@ packages: resolution: {integrity: sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==} engines: {node: '>=12.17'} - array-bounds@1.0.1: - resolution: {integrity: sha512-8wdW3ZGk6UjMPJx/glyEt0sLzzwAE1bhToPsO1W2pbpR2gULyxe3BjSiuJFheP50T/GgODVPz2fuMUmIywt8cQ==} - - array-find-index@1.0.2: - resolution: {integrity: sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==} - engines: {node: '>=0.10.0'} - array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} array-ify@1.0.0: resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} - array-normalize@1.1.4: - resolution: {integrity: sha512-fCp0wKFLjvSPmCn4F5Tiw4M3lpMZoHlCjfcs7nNzuj3vqQQ1/a8cgB9DXcpDSn18c+coLnaW7rqfcYCvKbyJXg==} - - array-range@1.0.1: - resolution: {integrity: sha512-shdaI1zT3CVNL2hnx9c0JMc0ZogGaxDs5e85akgHWKYa0yVbIyp06Ind3dVkTj/uuFrzaHBOyqFzo+VV6aXgtA==} - array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -5992,10 +5468,6 @@ packages: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} - ast-types@0.16.1: - resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} - engines: {node: '>=4'} - astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} @@ -6010,13 +5482,6 @@ packages: autocomplete.js@0.37.1: resolution: {integrity: sha512-PgSe9fHYhZEsm/9jggbjtVsGXJkPLvd+9mC7gZJ662vVL5CRWEtm/mIrrzCx0MrNxHVwxD5d00UOn6NsmL2LUQ==} - autoprefixer@10.4.21: - resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 - autoprefixer@10.4.23: resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==} engines: {node: ^10 || ^12 || >=14} @@ -6028,9 +5493,6 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - babel-dead-code-elimination@1.0.12: - resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} - babel-loader@9.2.1: resolution: {integrity: sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==} engines: {node: '>= 14.15.0'} @@ -6069,10 +5531,6 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - base64-arraybuffer@1.0.2: - resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} - engines: {node: '>= 0.6.0'} - base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -6114,21 +5572,9 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - binary-search-bounds@2.0.5: - resolution: {integrity: sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==} - birpc@4.0.0: resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} - bit-twiddle@1.0.2: - resolution: {integrity: sha512-B9UhK0DKFZhoTFcfvAzhqsjStvGJp9vYWf3+6SNTtdSQnvIgfkHbgHrg/e4+TH71N2GDu8tpmCVoyfrL1d7ntA==} - - bitmap-sdf@1.0.4: - resolution: {integrity: sha512-1G3U4n5JE6RAiALMxu0p1XmeZkTeCwGKykzsLTCqVzfSDaN6S7fKnkIkfejogz+iwqBWc0UYAIKnKHNN7pSfDg==} - - bl@2.2.1: - resolution: {integrity: sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==} - bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -6266,9 +5712,6 @@ packages: caniuse-lite@1.0.30001760: resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==} - canvas-fit@1.5.0: - resolution: {integrity: sha512-onIcjRpz69/Hx5bB5HGbYKUF2uC6QT6Gp+pfpGm3A7mPfcluSLV5v4Zu+oflDUwLdUw0rLIBhUbi0v8hM4FJQQ==} - ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -6363,9 +5806,6 @@ packages: cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} - clamp@1.0.1: - resolution: {integrity: sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA==} - class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -6430,37 +5870,13 @@ packages: collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} - color-alpha@1.0.4: - resolution: {integrity: sha512-lr8/t5NPozTSqli+duAN+x+no/2WaKTeWvxhHGN+aXT6AJ8vPlzLa7UriyjWak0pSC2jHol9JgjBYnnHsGha9A==} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} - color-id@1.1.0: - resolution: {integrity: sha512-2iRtAn6dC/6/G7bBIo0uupVrIne1NsQJvJxZOBCzQOfk7jRq97feaDZ3RdzuHakRXXnHGNwglto3pqtRx1sX0g==} - color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - color-normalize@1.5.0: - resolution: {integrity: sha512-rUT/HDXMr6RFffrR53oX3HGWkDOP9goSAQGBkUaAYKjOE2JxozccdGyufageWDlInRAjm/jYPrf/Y38oa+7obw==} - - color-parse@1.4.3: - resolution: {integrity: sha512-BADfVl/FHkQkyo8sRBwMYBqemqsgnu7JZAwUgvBvuwwuNUZAhSvLTbsEErS5bQXzOjDR0dWzJ4vXN2Q+QoPx0A==} - - color-parse@2.0.0: - resolution: {integrity: sha512-g2Z+QnWsdHLppAbrpcFWo629kLOnOPtpxYV69GCqm92gqSgyXbzlfyN3MXs0412fPBkFmiuS+rXposgBgBa6Kg==} - - color-rgba@2.4.0: - resolution: {integrity: sha512-Nti4qbzr/z2LbUWySr7H9dk3Rl7gZt7ihHAxlgT4Ho90EXWkjtkL1avTleu9yeGuqrt/chxTB6GKK8nZZ6V0+Q==} - - color-rgba@3.0.0: - resolution: {integrity: sha512-PPwZYkEY3M2THEHHV6Y95sGUie77S7X8v+h1r6LSAPF3/LL2xJ8duUXSrkic31Nzc4odPwHgUbiX/XuTYzQHQg==} - - color-space@2.3.2: - resolution: {integrity: sha512-BcKnbOEsOarCwyoLstcoEztwT0IJxqqQkNwDuA3a65sICvvHL2yoeV13psoDFh5IuiOMnIOKdQDwB4Mk3BypiA==} - color-support@1.1.3: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true @@ -6541,10 +5957,6 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - concat-stream@1.6.2: - resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} - engines: {'0': node >= 0.8} - concat-stream@2.0.0: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} @@ -6637,9 +6049,6 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-es@2.0.1: - resolution: {integrity: sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA==} - cookie-signature@1.0.7: resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} @@ -6697,9 +6106,6 @@ packages: typescript: optional: true - country-regex@1.1.0: - resolution: {integrity: sha512-iSPlClZP8vX7MC3/u6s3lrDuoQyhQukh5LyABJ3hvfzbQ3Yyayd4fp04zjLnfi267B/B2FkumcWWgrbban7sSA==} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -6720,24 +6126,6 @@ packages: peerDependencies: postcss: ^8.0.9 - css-font-size-keywords@1.0.0: - resolution: {integrity: sha512-Q+svMDbMlelgCfH/RVDKtTDaf5021O486ZThQPIpahnIjUkMUslC+WuOQSWTgGSrNCH08Y7tYNEmmy0hkfMI8Q==} - - css-font-stretch-keywords@1.0.1: - resolution: {integrity: sha512-KmugPO2BNqoyp9zmBIUGwt58UQSfyk1X5DbOlkb2pckDXFSAfjsD5wenb88fNrD6fvS+vu90a/tsPpb9vb0SLg==} - - css-font-style-keywords@1.0.1: - resolution: {integrity: sha512-0Fn0aTpcDktnR1RzaBYorIxQily85M2KXRpzmxQPgh8pxUN9Fcn00I8u9I3grNr1QXVgCl9T5Imx0ZwKU973Vg==} - - css-font-weight-keywords@1.0.0: - resolution: {integrity: sha512-5So8/NH+oDD+EzsnF4iaG4ZFHQ3vaViePkL1ZbZ5iC/KrsCY+WHq/lvOgrtmuOQ9pBBZ1ADGpaf+A4lj1Z9eYA==} - - css-font@1.2.0: - resolution: {integrity: sha512-V4U4Wps4dPDACJ4WpgofJ2RT5Yqwe1lEH6wlOOaIxMi0gTjdIijsc5FmxQlZ7ZZyKQkkutqqvULOp07l9c7ssA==} - - css-global-keywords@1.0.1: - resolution: {integrity: sha512-X1xgQhkZ9n94WDwntqst5D/FKkmiU0GlJSFZSV3kLvyJ1WC5VeyoXDOuleUD+SIuH9C7W05is++0Woh0CGfKjQ==} - css-has-pseudo@7.0.3: resolution: {integrity: sha512-oG+vKuGyqe/xvEMoxAQrhi7uY16deJR3i7wwhBerVrGQKSqUC5GiOVxTpM9F9B9hw0J+eKeOWLH7E9gZ1Dr5rA==} engines: {node: '>=18'} @@ -6799,9 +6187,6 @@ packages: css-selector-parser@3.3.0: resolution: {integrity: sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==} - css-system-font-keywords@1.0.0: - resolution: {integrity: sha512-1umTtVd/fXS25ftfjB71eASCrYhilmEsvDEI6wG/QplnmlfmVM5HkZ/ZX46DT5K3eblFPgLUHt5BRCb0YXkSFA==} - css-tree@2.2.1: resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} @@ -6818,9 +6203,6 @@ packages: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} - csscolorparser@1.0.3: - resolution: {integrity: sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==} - cssdb@8.5.2: resolution: {integrity: sha512-Pmoj9RmD8RIoIzA2EQWO4D4RMeDts0tgAH0VXdlNdxjuBGI3a9wMOIcUwaPNmD4r2qtIa06gqkIf7sECl+cBCg==} @@ -6878,9 +6260,6 @@ packages: resolution: {integrity: sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==} engines: {node: '>=0.10'} - d3-array@1.2.4: - resolution: {integrity: sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==} - d3-array@2.12.1: resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} @@ -6900,9 +6279,6 @@ packages: resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} engines: {node: '>=12'} - d3-collection@1.0.7: - resolution: {integrity: sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==} - d3-color@3.1.0: resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} engines: {node: '>=12'} @@ -6915,9 +6291,6 @@ packages: resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} engines: {node: '>=12'} - d3-dispatch@1.0.6: - resolution: {integrity: sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==} - d3-dispatch@3.0.1: resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} engines: {node: '>=12'} @@ -6939,34 +6312,18 @@ packages: resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} engines: {node: '>=12'} - d3-force@1.2.1: - resolution: {integrity: sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==} - d3-force@3.0.0: resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} engines: {node: '>=12'} - d3-format@1.4.5: - resolution: {integrity: sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==} - d3-format@3.1.0: resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} engines: {node: '>=12'} - d3-geo-projection@2.9.0: - resolution: {integrity: sha512-ZULvK/zBn87of5rWAfFMc9mJOipeSo57O+BBitsKIXmU4rTVAnX1kSsJkE0R+TxY8pGNoM1nbyRRE7GYHhdOEQ==} - hasBin: true - - d3-geo@1.12.1: - resolution: {integrity: sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==} - d3-geo@3.1.1: resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} engines: {node: '>=12'} - d3-hierarchy@1.1.9: - resolution: {integrity: sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==} - d3-hierarchy@3.1.2: resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} engines: {node: '>=12'} @@ -6986,9 +6343,6 @@ packages: resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} engines: {node: '>=12'} - d3-quadtree@1.0.7: - resolution: {integrity: sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==} - d3-quadtree@3.0.1: resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} engines: {node: '>=12'} @@ -7019,23 +6373,14 @@ packages: resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} engines: {node: '>=12'} - d3-time-format@2.3.0: - resolution: {integrity: sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==} - d3-time-format@4.1.0: resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} engines: {node: '>=12'} - d3-time@1.1.0: - resolution: {integrity: sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==} - d3-time@3.1.0: resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} engines: {node: '>=12'} - d3-timer@1.0.10: - resolution: {integrity: sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==} - d3-timer@3.0.1: resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} engines: {node: '>=12'} @@ -7054,10 +6399,6 @@ packages: resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} engines: {node: '>=12'} - d@1.0.2: - resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} - engines: {node: '>=0.12'} - dagre-d3-es@7.0.13: resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==} @@ -7097,14 +6438,6 @@ packages: supports-color: optional: true - debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -7182,9 +6515,6 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} - defined@1.0.1: - resolution: {integrity: sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==} - defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} @@ -7214,9 +6544,6 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - detect-kerning@2.1.2: - resolution: {integrity: sha512-I3JIbrnKPAntNLl1I6TpSQQdQ4AutYzv/sKMFKbepawV/hlH0GmYKhUoOEMd4xqaUHT+Bm0f4127lh5qs1m1tw==} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -7235,10 +6562,6 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - diff@8.0.4: - resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} - engines: {node: '>=0.3.1'} - dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -7326,9 +6649,6 @@ packages: resolution: {integrity: sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - draw-svg-path@1.0.0: - resolution: {integrity: sha512-P8j3IHxcgRMcY6sDzr0QvJDLzBnJJqpTG33UZ2Pvp8rw0apCHhJCWqYprqrXjrgHnJ6tuhP1iTJSAodPDHxwkg==} - drizzle-orm@0.45.1: resolution: {integrity: sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA==} peerDependencies: @@ -7430,29 +6750,13 @@ packages: oxc-resolver: optional: true - dtype@2.0.0: - resolution: {integrity: sha512-s2YVcLKdFGS0hpFqJaTwscsyt0E8nNFdmo73Ocd81xNPj4URI4rj6D60A+vFMIw7BXWlb4yRkEwfBqcZzPGiZg==} - engines: {node: '>= 0.8.0'} - dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - dup@1.0.0: - resolution: {integrity: sha512-Bz5jxMMC0wgp23Zm15ip1x8IhYRqJvF3nFC0UInJUDkN1z4uNPk9jTnfCUJXbOGiQ1JbXLQsiV41Fb+HXcj5BA==} - duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - duplexify@3.7.1: - resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} - - earcut@2.2.4: - resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==} - - earcut@3.0.2: - resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==} - eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -7478,12 +6782,6 @@ packages: electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} - element-size@1.1.1: - resolution: {integrity: sha512-eaN+GMOq/Q+BIWy0ybsgpcYImjGIdNLyjLFJU4XsLHXYQao5jCNb36GyN6C2qwmDDYSfIBmKpPpr4VnBdLCsPQ==} - - elementary-circuits-directed-graph@1.3.1: - resolution: {integrity: sha512-ZEiB5qkn2adYmpXGnJKkxT8uJHlW/mxmBpmeqawEHzPxh9HkLD4/1mFYX5l0On+f6rcPIt8/EWlRU2Vo3fX6dQ==} - embla-carousel-react@8.6.0: resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==} peerDependencies: @@ -7579,23 +6877,6 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} - es-toolkit@1.46.0: - resolution: {integrity: sha512-IToJ6ct9OLl5zz6WsC/1vZEwfSZ7Myil+ygl5Tf30Xjn9AEkzNB4kqp2G7VUJKF1DtTx/ra5M5KLlXvzOg51BA==} - - es5-ext@0.10.64: - resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} - engines: {node: '>=0.10'} - - es6-iterator@2.0.3: - resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} - - es6-symbol@3.1.4: - resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} - engines: {node: '>=0.12'} - - es6-weak-map@2.0.3: - resolution: {integrity: sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==} - esast-util-from-estree@2.0.0: resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} @@ -7635,23 +6916,12 @@ packages: engines: {node: '>=6.0'} hasBin: true - eslint-plugin-react-hooks@5.2.0: - resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} - engines: {node: '>=10'} - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - eslint-plugin-react-hooks@7.0.1: resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} engines: {node: '>=18'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - eslint-plugin-react-refresh@0.4.22: - resolution: {integrity: sha512-atkAG6QaJMGoTLc4MDAP+rqZcfwQuTIh2IqHWFLy2TEjxr0MOK+5BSG4RzL2564AAPpZkDRsZXAUz68kjnU6Ug==} - peerDependencies: - eslint: '>=8.40' - eslint-plugin-react-refresh@0.4.24: resolution: {integrity: sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==} peerDependencies: @@ -7673,16 +6943,6 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.36.0: - resolution: {integrity: sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - eslint@9.39.1: resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -7693,10 +6953,6 @@ packages: jiti: optional: true - esniff@2.0.1: - resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} - engines: {node: '>=0.10'} - espree@10.4.0: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -7766,9 +7022,6 @@ packages: resolution: {integrity: sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==} engines: {node: '>= 0.8'} - event-emitter@0.3.5: - resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} - eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} @@ -7809,9 +7062,6 @@ packages: exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} - ext@1.7.0: - resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} - extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} @@ -7819,10 +7069,6 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - falafel@2.2.5: - resolution: {integrity: sha512-HuC1qF9iTnHDnML9YZAdCDQwT0yKl/U55K4XSUXqGAA2GLoafFgWRqdAbhWJxXaYD4pyoVxAJ8wH670jMpI9DQ==} - engines: {node: '>=0.4.0'} - fast-content-type-parse@3.0.0: resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} @@ -7837,9 +7083,6 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-isnumeric@1.1.4: - resolution: {integrity: sha512-1mM8qOr2LYz8zGaUdmiqRDiuue00Dxjgcb1NQR7TnhLVh6sQyngP9xvLo7Sl7LZpP/sk5eb+bcyWXw530NTBZw==} - fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -7944,9 +7187,6 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - flatten-vertex-data@1.0.2: - resolution: {integrity: sha512-BvCBFK2NZqerFTdMDgqfHBwxYWnxeCkwONsw6PvBMcUXqo8U/KDWwmXhqx1x2kLIg7DqIsJfOaJFOmlua3Lxuw==} - follow-redirects@1.15.11: resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} @@ -7956,12 +7196,6 @@ packages: debug: optional: true - font-atlas@2.1.0: - resolution: {integrity: sha512-kP3AmvX+HJpW4w3d+PiPR2X6E1yvsBXt2yhuCw+yReO9F1WYhvZwx3c95DGZGwg9xYzDGrgJYa885xmVA+28Cg==} - - font-measure@1.2.2: - resolution: {integrity: sha512-mRLEpdrWzKe9hbfaF3Qpr06TAjquuBVP5cHy4b3hyeNdjc9i0PO6HniGsX5vjL5OWv7+Bd++NiooNpT/s8BvIA==} - for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -7998,9 +7232,6 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} - fraction.js@4.3.7: - resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} @@ -8008,9 +7239,6 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} - from2@2.3.0: - resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} - fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -8060,19 +7288,10 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} - geojson-vt@3.2.1: - resolution: {integrity: sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==} - - geojson-vt@4.0.2: - resolution: {integrity: sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==} - get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-canvas-context@1.0.2: - resolution: {integrity: sha512-LnpfLf/TNzr9zVOGiIY6aKCz8EKuXmlYNV7CM2pUjBa/B+c2I15tS7KLySep75+FuerJdmArvJLcsAXWEy2H0A==} - get-east-asian-width@1.4.0: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} @@ -8133,18 +7352,6 @@ packages: github-slugger@1.5.0: resolution: {integrity: sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==} - gl-mat4@1.2.0: - resolution: {integrity: sha512-sT5C0pwB1/e9G9AvAoLsoaJtbMGjfd/jfxo8jMCKqYYEnjZuFvqV5rehqar0538EmssjdDeiEWnKyBSTw7quoA==} - - gl-matrix@3.4.4: - resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==} - - gl-text@1.4.0: - resolution: {integrity: sha512-o47+XBqLCj1efmuNyCHt7/UEJmB9l66ql7pnobD6p+sgmBUdzfMZXIF0zD2+KRfpd99DJN+QXdvTFAGCKCVSmQ==} - - gl-util@3.1.3: - resolution: {integrity: sha512-dvRTggw5MSkJnCbh74jZzSoTOGnVYK+Bt+Ckqm39CVcl6+zSsxqWk4lr5NKhkqXHL6qvZAU9h17ZF8mIskY9mA==} - glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -8184,18 +7391,10 @@ packages: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} engines: {node: '>=10'} - global-prefix@4.0.0: - resolution: {integrity: sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==} - engines: {node: '>=16'} - globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} - globals@16.4.0: - resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} - engines: {node: '>=18'} - globals@16.5.0: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} @@ -8211,57 +7410,6 @@ packages: globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - glsl-inject-defines@1.0.3: - resolution: {integrity: sha512-W49jIhuDtF6w+7wCMcClk27a2hq8znvHtlGnrYkSWEr8tHe9eA2dcnohlcAmxLYBSpSSdzOkRdyPTrx9fw49+A==} - - glsl-resolve@0.0.1: - resolution: {integrity: sha512-xxFNsfnhZTK9NBhzJjSBGX6IOqYpvBHxxmo+4vapiljyGNCY0Bekzn0firQkQrazK59c1hYxMDxYS8MDlhw4gA==} - - glsl-token-assignments@2.0.2: - resolution: {integrity: sha512-OwXrxixCyHzzA0U2g4btSNAyB2Dx8XrztY5aVUCjRSh4/D0WoJn8Qdps7Xub3sz6zE73W3szLrmWtQ7QMpeHEQ==} - - glsl-token-defines@1.0.0: - resolution: {integrity: sha512-Vb5QMVeLjmOwvvOJuPNg3vnRlffscq2/qvIuTpMzuO/7s5kT+63iL6Dfo2FYLWbzuiycWpbC0/KV0biqFwHxaQ==} - - glsl-token-depth@1.1.2: - resolution: {integrity: sha512-eQnIBLc7vFf8axF9aoi/xW37LSWd2hCQr/3sZui8aBJnksq9C7zMeUYHVJWMhFzXrBU7fgIqni4EhXVW4/krpg==} - - glsl-token-descope@1.0.2: - resolution: {integrity: sha512-kS2PTWkvi/YOeicVjXGgX5j7+8N7e56srNDEHDTVZ1dcESmbmpmgrnpjPcjxJjMxh56mSXYoFdZqb90gXkGjQw==} - - glsl-token-inject-block@1.1.0: - resolution: {integrity: sha512-q/m+ukdUBuHCOtLhSr0uFb/qYQr4/oKrPSdIK2C4TD+qLaJvqM9wfXIF/OOBjuSA3pUoYHurVRNao6LTVVUPWA==} - - glsl-token-properties@1.0.1: - resolution: {integrity: sha512-dSeW1cOIzbuUoYH0y+nxzwK9S9O3wsjttkq5ij9ZGw0OS41BirKJzzH48VLm8qLg+au6b0sINxGC0IrGwtQUcA==} - - glsl-token-scope@1.1.2: - resolution: {integrity: sha512-YKyOMk1B/tz9BwYUdfDoHvMIYTGtVv2vbDSLh94PT4+f87z21FVdou1KNKgF+nECBTo0fJ20dpm0B1vZB1Q03A==} - - glsl-token-string@1.0.1: - resolution: {integrity: sha512-1mtQ47Uxd47wrovl+T6RshKGkRRCYWhnELmkEcUAPALWGTFe2XZpH3r45XAwL2B6v+l0KNsCnoaZCSnhzKEksg==} - - glsl-token-whitespace-trim@1.0.0: - resolution: {integrity: sha512-ZJtsPut/aDaUdLUNtmBYhaCmhIjpKNg7IgZSfX5wFReMc2vnj8zok+gB/3Quqs0TsBSX/fGnqUUYZDqyuc2xLQ==} - - glsl-tokenizer@2.1.5: - resolution: {integrity: sha512-XSZEJ/i4dmz3Pmbnpsy3cKh7cotvFlBiZnDOwnj/05EwNp2XrhQ4XKJxT7/pDt4kp4YcpRSKz8eTV7S+mwV6MA==} - - glslify-bundle@5.1.1: - resolution: {integrity: sha512-plaAOQPv62M1r3OsWf2UbjN0hUYAB7Aph5bfH58VxJZJhloRNbxOL9tl/7H71K7OLJoSJ2ZqWOKk3ttQ6wy24A==} - - glslify-deps@1.3.2: - resolution: {integrity: sha512-7S7IkHWygJRjcawveXQjRXLO2FTjijPDYC7QfZyAQanY+yGLCFHYnPtsGT9bdyHiwPTw/5a1m1M9hamT2aBpag==} - - glslify@7.1.1: - resolution: {integrity: sha512-bud98CJ6kGZcP9Yxcsi7Iz647wuDz3oN+IZsjCRi5X1PI7t/xPKeL0mOwXJjo+CRZMqvq0CkSJiywCcY7kVYog==} - hasBin: true - - goober@2.1.18: - resolution: {integrity: sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==} - peerDependencies: - csstype: ^3.0.10 - google-auth-library@10.5.0: resolution: {integrity: sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==} engines: {node: '>=18'} @@ -8292,16 +7440,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - gray-matter@4.0.3: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} - grid-index@1.1.0: - resolution: {integrity: sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==} - gtoken@8.0.0: resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==} engines: {node: '>=18'} @@ -8325,12 +7467,6 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - has-hover@1.0.1: - resolution: {integrity: sha512-0G6w7LnlcpyDzpeGUTuT0CEw05+QlMuGVk1IHNAlHrGJITGodjZu3x8BNDUMfKJSZXNB2ZAclqc1bvrd+uUpfg==} - - has-passive-events@1.0.0: - resolution: {integrity: sha512-2vSj6IeIsgvsRMyeQ0JaCX5Q3lX4zMn5HpoVc7MEhQ6pv8Iq9rsXjsp+E5ZwaT7T0xhMT0KmU8gtt1EFVdbJiw==} - has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} @@ -8629,12 +7765,6 @@ packages: immediate@3.3.0: resolution: {integrity: sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==} - immer@10.2.0: - resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} - - immer@11.1.4: - resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==} - import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -8686,10 +7816,6 @@ packages: resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - ini@4.1.3: - resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - ini@6.0.0: resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} engines: {node: ^20.17.0 || >=22.9.0} @@ -8747,9 +7873,6 @@ packages: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} - is-browser@2.1.0: - resolution: {integrity: sha512-F5rTJxDQ2sW81fcfOR1GnCXT6sVJC104fCyfj+mjpwNEwaPYSn5fte5jiHmBg3DHsIoL/l8Kvw5VN5SsTRcRFQ==} - is-buffer@2.0.5: resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} engines: {node: '>=4'} @@ -8787,14 +7910,6 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - is-finite@1.1.0: - resolution: {integrity: sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==} - engines: {node: '>=0.10.0'} - - is-firefox@1.0.3: - resolution: {integrity: sha512-6Q9ITjvWIm0Xdqv+5U12wgOKEM2KoBw4Y926m0OFkvlCxnbG94HKAsVz8w3fWcfAS5YA2fJORXX1dLrkprCCxA==} - engines: {node: '>=0.10.0'} - is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -8827,9 +7942,6 @@ packages: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} engines: {node: '>=12'} - is-mobile@4.0.0: - resolution: {integrity: sha512-mlcHZA84t1qLSuWkt2v0I2l61PYdyQDt4aG1mLIXF5FDMm4+haBCxCPYSr/uwqQNRk1MiTizn0ypEuRAOLRAew==} - is-network-error@1.3.0: resolution: {integrity: sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==} engines: {node: '>=16'} @@ -8854,10 +7966,6 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} - is-plain-obj@1.1.0: - resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} - engines: {node: '>=0.10.0'} - is-plain-obj@2.1.0: resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} engines: {node: '>=8'} @@ -8896,12 +8004,6 @@ packages: resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} engines: {node: '>=18'} - is-string-blank@1.0.1: - resolution: {integrity: sha512-9H+ZBCVs3L9OYqv8nuUAzpcT9OTgMD1yAWrG7ihlnibdkbtB850heAmYWxHuXc4CHy4lKeK69tN+ny1K7gBIrw==} - - is-svg-path@1.0.2: - resolution: {integrity: sha512-Lj4vePmqpPR1ZnRctHv8ltSh1OrSxHkhUkd7wi+VQdcdP15/KvQFyk7LhNuM7ZW0EVbJz8kZLVmL9quLrfq4Kg==} - is-text-path@2.0.0: resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} engines: {node: '>=8'} @@ -8938,17 +8040,9 @@ packages: isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - isbot@5.1.39: - resolution: {integrity: sha512-obH0yYahGXdzNxo+djmHhBYThUKDkz565cxkIlt2L9hXfv1NlaLKoDBHo6KxXsYrIXx2RK3x5vY36CfZcobxEw==} - engines: {node: '>=18'} - isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isexe@3.1.5: - resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} - engines: {node: '>=18'} - isexe@4.0.0: resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} engines: {node: '>=20'} @@ -9072,9 +8166,6 @@ packages: json-stringify-nice@1.1.4: resolution: {integrity: sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw==} - json-stringify-pretty-compact@4.0.0: - resolution: {integrity: sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==} - json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -9110,12 +8201,6 @@ packages: resolution: {integrity: sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==} hasBin: true - kdbush@3.0.0: - resolution: {integrity: sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==} - - kdbush@4.0.2: - resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==} - keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -9374,11 +8459,6 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} - lucide-react@0.546.0: - resolution: {integrity: sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ==} - peerDependencies: - react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - lucide-react@0.554.0: resolution: {integrity: sha512-St+z29uthEJVx0Is7ellNkgTEhaeSoA42I7JjOCBCrc5X6LYMGSv0P/2uS5HDLTExP5tpiqRD2PyUEOS6s9UXA==} peerDependencies: @@ -9412,17 +8492,6 @@ packages: resolution: {integrity: sha512-uCbIa8jWWmQZt4dSnEStkVC6gdakiinAm4PiGsywIkguF0eWMdcjDz0ECYhUolFU3pFLOev9VNPCEygydXnddg==} engines: {node: ^20.17.0 || >=22.9.0} - map-limit@0.0.1: - resolution: {integrity: sha512-pJpcfLPnIF/Sk3taPW21G/RQsEEirGaFpCW3oXRwH9dnFHPHNGjNyvh++rdmC2fNqEaTw2MhYJraoJWAHx8kEg==} - - mapbox-gl@1.13.3: - resolution: {integrity: sha512-p8lJFEiqmEQlyv+DQxFAOG/XPWN0Wp7j/Psq93Zywz7qt9CcUKFYDBOoOEKzqe6gudHVJY8/Bhqw6VDpX2lSBg==} - engines: {node: '>=6.4.0'} - - maplibre-gl@4.7.1: - resolution: {integrity: sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==} - engines: {node: '>=16.14.0', npm: '>=8.1.0'} - mark.js@8.11.1: resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} @@ -9454,10 +8523,6 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - math-log2@1.0.1: - resolution: {integrity: sha512-9W0yGtkaMAkf74XGYVy4Dqw3YUMnTNB2eeiw9aQbUl4A3KmuCEHTt2DgAB07ENzOYAjsYSAYufkAq0Zd+jU7zA==} - engines: {node: '>=0.10.0'} - mdast-util-directive@3.1.0: resolution: {integrity: sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==} @@ -9819,18 +8884,6 @@ packages: moment@2.30.1: resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} - mouse-change@1.4.0: - resolution: {integrity: sha512-vpN0s+zLL2ykyyUDh+fayu9Xkor5v/zRD9jhSqjRS1cJTGS0+oakVZzNm5n19JvvEj0you+MXlYTpNxUDQUjkQ==} - - mouse-event-offset@3.0.2: - resolution: {integrity: sha512-s9sqOs5B1Ykox3Xo8b3Ss2IQju4UwlW6LSR+Q5FXWpprJ5fzMLefIIItr3PH8RwzfGy6gxs/4GAmiNuZScE25w==} - - mouse-event@1.0.5: - resolution: {integrity: sha512-ItUxtL2IkeSKSp9cyaX2JLUuKk2uMoxBg4bbOWVd29+CskYJR9BGsUqtXenNzKbnDshvupjUewDIYVrOB6NmGw==} - - mouse-wheel@1.2.0: - resolution: {integrity: sha512-+OfYBiUOCTWcTECES49neZwL5AoGkXE+lFjIvzwNCnYRlso+EnfvovcBxGoyQ0yQt806eSPjS675K0EwWknXmw==} - mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -9849,9 +8902,6 @@ packages: resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} hasBin: true - murmurhash-js@1.0.0: - resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==} - mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} @@ -9864,17 +8914,9 @@ packages: napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} - native-promise-only@0.8.1: - resolution: {integrity: sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==} - natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - needle@2.9.1: - resolution: {integrity: sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==} - engines: {node: '>= 4.4.x'} - hasBin: true - negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} @@ -9904,9 +8946,6 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next-tick@1.1.0: - resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} - no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} @@ -9980,16 +9019,6 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - - normalize-svg-path@0.1.0: - resolution: {integrity: sha512-1/kmYej2iedi5+ROxkRESL/pI02pkg0OBnaR4hJkSIX6+ORzepwbuUXfrdZaPjysTsJInj0Rj5NuX027+dMBvA==} - - normalize-svg-path@1.1.0: - resolution: {integrity: sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==} - normalize-url@8.1.0: resolution: {integrity: sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==} engines: {node: '>=14.16'} @@ -10037,10 +9066,6 @@ packages: peerDependencies: webpack: ^4.0.0 || ^5.0.0 - number-is-integer@1.0.1: - resolution: {integrity: sha512-Dq3iuiFBkrbmuQjGFFF3zckXNCQoSD37/SdSbgcBailUx6knDvDwb5CympBgcoWHy36sfS12u74MHYkXyHq6bg==} - engines: {node: '>=0.10.0'} - nypm@0.6.2: resolution: {integrity: sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==} engines: {node: ^14.16.0 || >=16.10.0} @@ -10083,9 +9108,6 @@ packages: resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} engines: {node: '>= 0.8'} - once@1.3.3: - resolution: {integrity: sha512-6vaNInhu+CHxtONf3zw3vq4SP2DOQhjBvIa3rNcG0+P7eKWlYH6Peu7rHizSloRU2EwMz6GraLieis9Ac9+p1w==} - once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -10101,12 +9123,6 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} - oniguruma-parser@0.12.2: - resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} - - oniguruma-to-es@4.3.6: - resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} - open@10.2.0: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} @@ -10210,9 +9226,6 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} - parenthesis@3.1.8: - resolution: {integrity: sha512-KF/U8tk54BgQewkJPvB4s/US3VQY68BRDpH638+7O/n58TpnwiwnOtGIOsT2/i+M78s61BBpeC83STB88d8sqw==} - parse-conflict-json@5.0.1: resolution: {integrity: sha512-ZHEmNKMq1wyJXNwLxyHnluPfRAFSIliBvbK/UiOceROt4Xh9Pz0fq49NytIaeaCUf5VR86hwQ/34FCcNU5/LKQ==} engines: {node: ^20.17.0 || >=22.9.0} @@ -10230,15 +9243,6 @@ packages: parse-path@7.1.0: resolution: {integrity: sha512-EuCycjZtfPcjWk7KTksnJ5xPMvWGA/6i4zrLYhRG0hGvC3GPU/jGUj3Cy+ZR0v30duV3e23R95T1lE2+lsndSw==} - parse-rect@1.2.0: - resolution: {integrity: sha512-4QZ6KYbnE6RTwg9E0HpLchUM9EZt6DnDxajFZZDSV4p/12ZJEvPO702DZpGvRYEPo00yKDys7jASi+/w7aO8LA==} - - parse-svg-path@0.1.2: - resolution: {integrity: sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==} - - parse-unit@1.0.1: - resolution: {integrity: sha512-hrqldJHokR3Qj88EIlV/kAyAi/G5R2+R56TBANxNMy0uPlYcttx0jnMW6Yx5KsKPSbC3KddM/7qQm3+0wEXKxg==} - parse-url@9.2.0: resolution: {integrity: sha512-bCgsFI+GeGWPAvAiUv63ZorMeif3/U0zaXABGJbOWt5OH2KCaPHF6S+0ok4aqM9RuIPGyZdx9tR9l13PsW4AYQ==} engines: {node: '>=14.13.0'} @@ -10315,16 +9319,9 @@ packages: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} - pbf@3.3.0: - resolution: {integrity: sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==} - hasBin: true - perfect-debounce@2.0.0: resolution: {integrity: sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==} - performance-now@2.1.0: - resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} - pg-cloudflare@1.3.0: resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} @@ -10359,9 +9356,6 @@ packages: pgpass@1.0.5: resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} - pick-by-alias@1.2.0: - resolution: {integrity: sha512-ESj2+eBxhGrcA1azgHs7lARG5+5iLakc/6nlfbpjcLl00HuuUOIuORhYXN4D1HfvMSKuVtFQjAlnwi1JHEeDIw==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -10398,22 +9392,12 @@ packages: engines: {node: '>=18'} hasBin: true - plotly.js@3.5.0: - resolution: {integrity: sha512-a3AYQIMG7OdZmrJ/fJ65HSt3g1l5qDeludKqjjafU1dh5E+fwqDhsEBndW7VCYwjlducCfN6KtPdWdiWFcoBWw==} - engines: {node: '>=18.0.0'} - - point-in-polygon@1.1.0: - resolution: {integrity: sha512-3ojrFwjnnw8Q9242TzgXuTD+eKiutbzyslcq1ydfu82Db2y+Ogbmyrkpv0Hgj31qwT3lbS9+QAAO/pIQM35XRw==} - points-on-curve@0.2.0: resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} points-on-path@0.2.1: resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} - polybooljs@1.2.2: - resolution: {integrity: sha512-ziHW/02J0XuNuUtmidBc6GXE8YohYydp3DWPWXYsd7O721TjcmN+k6ezjdwkDqep+gnWnFY+yqZHvzElra2oCg==} - possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -10825,12 +9809,6 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} - potpack@1.0.2: - resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} - - potpack@2.1.0: - resolution: {integrity: sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==} - prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} @@ -10866,9 +9844,6 @@ packages: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} - probe-image-size@7.2.3: - resolution: {integrity: sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w==} - proc-log@6.1.0: resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==} engines: {node: ^20.17.0 || >=22.9.0} @@ -10900,9 +9875,6 @@ packages: resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} engines: {node: '>=12.0.0'} - protocol-buffers-schema@3.6.1: - resolution: {integrity: sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==} - protocols@2.0.2: resolution: {integrity: sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==} @@ -10955,15 +9927,6 @@ packages: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} - quickselect@2.0.0: - resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==} - - quickselect@3.0.0: - resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==} - - raf@3.4.1: - resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} - randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -11037,24 +10000,6 @@ packages: react-loadable: '*' webpack: '>=4.41.1 || 5.x' - react-plotly.js@2.6.0: - resolution: {integrity: sha512-g93xcyhAVCSt9kV1svqG1clAEdL6k3U+jjuSzfTV7owaSU9Go6Ph8bl25J+jKfKvIGAEYpe4qj++WHJuc9IaeA==} - peerDependencies: - plotly.js: '>1.34.0' - react: '>0.13.0' - - react-redux@9.2.0: - resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} - peerDependencies: - '@types/react': ^18.2.25 || ^19 - react: ^18.0 || ^19 - redux: ^5.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - redux: - optional: true - react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} @@ -11139,9 +10084,6 @@ packages: resolution: {integrity: sha512-S16VePJnQcfmk6HIZAiP8TXW/VDlDtZfzVndRDE8lhZNA4YvAiwAjgvhoyf6+soofEH/vrZnOUctSt+jYE2tkg==} engines: {node: ^20.17.0 || >=22.9.0} - readable-stream@1.0.34: - resolution: {integrity: sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==} - readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -11157,10 +10099,6 @@ packages: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} - recast@0.23.11: - resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} - engines: {node: '>= 4'} - recharts-scale@0.4.5: resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} @@ -11171,14 +10109,6 @@ packages: react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - recharts@3.4.1: - resolution: {integrity: sha512-35kYg6JoOgwq8sE4rhYkVWwa6aAIgOtT+Ob0gitnShjwUwZmhrmy7Jco/5kJNF4PnLXgt9Hwq+geEMS+WrjU1g==} - engines: {node: '>=18'} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - recma-build-jsx@1.0.0: resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} @@ -11193,14 +10123,6 @@ packages: recma-stringify@1.0.0: resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} - redux-thunk@3.1.0: - resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} - peerDependencies: - redux: ^5.0.0 - - redux@5.0.1: - resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} - reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -11211,15 +10133,6 @@ packages: regenerate@1.4.2: resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} - regex-recursion@6.0.2: - resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} - - regex-utilities@2.3.0: - resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} - - regex@6.1.0: - resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} - regexpu-core@6.4.0: resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} engines: {node: '>=4'} @@ -11239,21 +10152,6 @@ packages: resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} hasBin: true - regl-error2d@2.0.12: - resolution: {integrity: sha512-r7BUprZoPO9AbyqM5qlJesrSRkl+hZnVKWKsVp7YhOl/3RIpi4UDGASGJY0puQ96u5fBYw/OlqV24IGcgJ0McA==} - - regl-line2d@3.1.3: - resolution: {integrity: sha512-fkgzW+tTn4QUQLpFKsUIE0sgWdCmXAM3ctXcCgoGBZTSX5FE2A0M7aynz7nrZT5baaftLrk9te54B+MEq4QcSA==} - - regl-scatter2d@3.4.0: - resolution: {integrity: sha512-DavKQlHsI+iHZuLgOL+yGkg+sPd94CS+7FCBWkcQ6s/TbaNfUsF9eN591fjjSWIoKrGNfb/SEGhsXR5lXjqZ2w==} - - regl-splom@1.0.14: - resolution: {integrity: sha512-OiLqjmPRYbd7kDlHC6/zDf6L8lxgDC65BhC8JirhP4ykrK4x22ZyS+BnY8EUinXKDeMgmpRwCvUmk7BK4Nweuw==} - - regl@2.1.1: - resolution: {integrity: sha512-+IOGrxl3FZ8ZM9ixCWQZzFRiRn7Rzn9bu3iFHwg/yz4tlOUQgbO4PHLgG+1ZT60zcIV8tief6Qrmyl8qcoJP0g==} - rehype-minify-whitespace@6.0.2: resolution: {integrity: sha512-Zk0pyQ06A3Lyxhe9vGtOtzz3Z0+qZ5+7icZ/PL/2x1SHPbKao5oB/g/rlc6BCTajqBb33JcOe71Ye1oFsuYbnw==} @@ -11331,9 +10229,6 @@ packages: requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - reselect@5.1.1: - resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} - resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} @@ -11351,12 +10246,6 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - resolve-protobuf-schema@2.1.0: - resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} - - resolve@0.6.3: - resolution: {integrity: sha512-UHBY3viPlJKf85YijDUcikKX6tmF4SokIDp518ZDVT92JNDcG5uKIthaT/owt3Sar0lwtOafsQuwrg22/v2Dwg==} - resolve@1.22.10: resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} engines: {node: '>= 0.4'} @@ -11388,9 +10277,6 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - right-now@1.0.0: - resolution: {integrity: sha512-DA8+YS+sMIVpbsuKgy+Z67L9Lxb1p05mNxRpDPNksPDEFir4vmBlUtuN9jkTGn9YMMdlBuK7XQgFiz6ws+yhSg==} - rimraf@5.0.10: resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} hasBin: true @@ -11620,16 +10506,6 @@ packages: serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - seroval-plugins@1.5.2: - resolution: {integrity: sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg==} - engines: {node: '>=10'} - peerDependencies: - seroval: ^1.0 - - seroval@1.5.2: - resolution: {integrity: sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==} - engines: {node: '>=10'} - serve-handler@6.1.6: resolution: {integrity: sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==} @@ -11660,9 +10536,6 @@ packages: resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} engines: {node: '>=8'} - shallow-copy@0.0.1: - resolution: {integrity: sha512-b6i4ZpVuUxB9h5gfCxPiusKYkqTMOjEbBs4wMaFbkfia4yFv92UKZ6Df8WXcKbn08JNL/abvg3FnMAOfakDvUw==} - shallowequal@1.1.0: resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} @@ -11678,9 +10551,6 @@ packages: resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} engines: {node: '>= 0.4'} - shiki@3.15.0: - resolution: {integrity: sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw==} - side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -11707,9 +10577,6 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - signum@1.0.0: - resolution: {integrity: sha512-yodFGwcyt59XRh7w5W3jPcIQb3Bwi21suEfT7MAWnBX3iCdklJpgDgvGT9o04UonglZN5SNMfJFkHIR/jO8GHw==} - simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} @@ -11777,9 +10644,6 @@ packages: resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} - solid-js@1.9.12: - resolution: {integrity: sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw==} - sonner@2.0.7: resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} peerDependencies: @@ -11852,15 +10716,9 @@ packages: resolution: {integrity: sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==} engines: {node: ^20.17.0 || >=22.9.0} - stack-trace@0.0.9: - resolution: {integrity: sha512-vjUc6sfgtgY0dxCdnc40mK6Oftjo9+2K8H/NG81TMhgL392FtiPA9tn9RLyTxXmTLPJPjF3VyzFp6bsWFLisMQ==} - stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - static-eval@2.1.1: - resolution: {integrity: sha512-MgWpQ/ZjGieSVB3eOJVs4OA2LT/q1vx98KPCTTQPzq/aLr0YUXTsgryTXr4SLfR0ZfUUCiedM9n/ABeDIyy4mA==} - statuses@1.5.0: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} @@ -11880,19 +10738,10 @@ packages: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} - stream-parser@0.3.1: - resolution: {integrity: sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==} - - stream-shift@1.0.3: - resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} - string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} - string-split-by@1.0.0: - resolution: {integrity: sha512-KaJKY+hfpzNyet/emP81PJA9hTVSfxNLS9SFTWxdCnnW1/zOOwiV248+EfoX7IQFcBaOp4G5YE6xTJMF+pLg6A==} - string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -11909,9 +10758,6 @@ packages: resolution: {integrity: sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==} engines: {node: '>=20'} - string_decoder@0.10.31: - resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} - string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} @@ -11960,9 +10806,6 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - strongly-connected-components@1.0.1: - resolution: {integrity: sha512-i0TFx4wPcO0FwX+4RkLJi1MxmcTv90jNZgxMu9XRnMXMeFUY1VJlIoXpZunPUvUUqbCT1pg5PEkFqqpcaElNaA==} - style-to-js@1.1.21: resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} @@ -11978,15 +10821,6 @@ packages: stylis@4.3.6: resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} - supercluster@7.1.5: - resolution: {integrity: sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==} - - supercluster@8.0.1: - resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==} - - superscript-text@1.0.0: - resolution: {integrity: sha512-gwu8l5MtRZ6koO0icVTlmN5pm7Dhh1+Xpe9O4x6ObMAsW+3jPbW14d1DsBq1F4wiI+WOFjXF35pslgec/G8yCQ==} - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -11999,18 +10833,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svg-arc-to-cubic-bezier@3.2.0: - resolution: {integrity: sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==} - svg-parser@2.0.4: resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} - svg-path-bounds@1.0.2: - resolution: {integrity: sha512-H4/uAgLWrppIC0kHsb2/dWUYSmb4GE5UqH06uqWBcg6LBjX2fu0A8+JrO2/FJPZiSsNOKZAhyFFgsLTdYUvSqQ==} - - svg-path-sdf@1.1.3: - resolution: {integrity: sha512-vJJjVq/R5lSr2KLfVXVAStktfcfa1pNFjFOgyJnzZFXlO/fDZ5DmM8FpnSKKzLPfEYTVeXuVBTHF296TpxuJVg==} - svgo@3.3.2: resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==} engines: {node: '>=14.0.0'} @@ -12032,9 +10857,6 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} - tailwind-merge@3.3.1: - resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} - tailwind-merge@3.4.0: resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} @@ -12103,12 +10925,6 @@ packages: resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} engines: {node: '>=18'} - through2@0.6.5: - resolution: {integrity: sha512-RkK/CCESdTKQZHdmKICijdKKsCRVHs5KsLZ6pACAmF/1GPUQhonHSXWNERctxEp7RmvjdNbZTL5z9V7nSCXKcg==} - - through2@2.0.5: - resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} - through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -12124,9 +10940,6 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinycolor2@1.6.0: - resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} - tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} @@ -12142,12 +10955,6 @@ packages: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} - tinyqueue@2.0.3: - resolution: {integrity: sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==} - - tinyqueue@3.0.0: - resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==} - tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} @@ -12167,12 +10974,6 @@ packages: resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} engines: {node: '>= 0.4'} - to-float32@1.1.0: - resolution: {integrity: sha512-keDnAusn/vc+R3iEiSDw8TOF7gPiTLdK1ArvWtYbJQiVfmRg6i/CAvbKq3uIS0vWroAC7ZecN3DjQKw3aSklUg==} - - to-px@1.0.1: - resolution: {integrity: sha512-2y3LjBeIZYL19e5gczp14/uRWFDtDUErJPVN3VU9a7SJO+RjGRtYR47aMN2bZgGlxvW4ZcEz2ddUPVHXcMfuXw==} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -12184,10 +10985,6 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - topojson-client@3.1.0: - resolution: {integrity: sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==} - hasBin: true - toposort-class@1.0.1: resolution: {integrity: sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==} @@ -12356,16 +11153,10 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} - type@2.7.3: - resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} - typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} - typedarray-pool@1.2.0: - resolution: {integrity: sha512-YTSQbzX43yvtpfRtIDAYygoYtgT+Rpjuxy9iOpczrjpXLgGoyG7aS5USJXV2d3nn8uHTeb9rXDvzS27zUg5KYQ==} - typedarray-to-buffer@3.1.5: resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} @@ -12445,13 +11236,6 @@ packages: typeorm-aurora-data-api-driver: optional: true - typescript-eslint@8.45.0: - resolution: {integrity: sha512-qzDmZw/Z5beNLUrXfd0HIW6MzIaAV5WNDxmMs9/3ojGOpYavofgNAAD/nC6tGV2PczIi0iw8vot2eAe/sBn7zg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - typescript-eslint@8.49.0: resolution: {integrity: sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -12592,13 +11376,6 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - unplugin@2.3.11: - resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} - engines: {node: '>=18.12.0'} - - unquote@1.1.1: - resolution: {integrity: sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==} - unrun@0.2.28: resolution: {integrity: sha512-LqMrI3ZEUMZ2476aCsbUTfy95CHByqez05nju4AQv4XFPkxh5yai7Di1/Qb0FoELHEEPDWhQi23EJeFyrBV0Og==} engines: {node: '>=20.19.0'} @@ -12615,9 +11392,6 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - update-diff@1.1.0: - resolution: {integrity: sha512-rCiBPiHxZwT4+sBhEbChzpO5hYHjm91kScWgdHf4Qeafs6Ba7MBl+d9GlGv72bcTZQO0sLmtQS1pHSWoCLtN/A==} - update-notifier@6.0.2: resolution: {integrity: sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==} engines: {node: '>=14.16'} @@ -12739,9 +11513,6 @@ packages: victory-vendor@36.9.2: resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} - victory-vendor@37.3.6: - resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} - vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -12843,9 +11614,6 @@ packages: vscode-uri@3.0.8: resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} - vt-pbf@3.1.3: - resolution: {integrity: sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==} - w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -12861,9 +11629,6 @@ packages: wbuf@1.7.3: resolution: {integrity: sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==} - weak-map@1.0.8: - resolution: {integrity: sha512-lNR9aAefbGPpHO7AEnY0hCFjz1eTkWCXYvkTRrTHs9qv8zJp+SkVYpzfLIFXQQiG3tVvbNFQgVg2bQS8YGgxyw==} - web-namespaces@1.1.4: resolution: {integrity: sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==} @@ -12874,9 +11639,6 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} - webgl-context@2.2.0: - resolution: {integrity: sha512-q/fGIivtqTT7PEoF07axFIlHNk/XCPaYpq64btnepopSWvKNFkoORlQYgqDigBIuGA1ExnFd/GnSUnBNEPQY7Q==} - webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -12923,9 +11685,6 @@ packages: resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} engines: {node: '>=10.13.0'} - webpack-virtual-modules@0.6.2: - resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - webpack@5.103.0: resolution: {integrity: sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==} engines: {node: '>=10.13.0'} @@ -12975,11 +11734,6 @@ packages: engines: {node: '>= 8'} hasBin: true - which@4.0.0: - resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} - engines: {node: ^16.13.0 || >=18.0.0} - hasBin: true - which@6.0.1: resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} engines: {node: ^20.17.0 || >=22.9.0} @@ -13021,9 +11775,6 @@ packages: resolution: {integrity: sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==} engines: {node: '>=12.17'} - world-calendars@1.0.4: - resolution: {integrity: sha512-VGRnLJS+xJmGDPodgJRnGIDwGu0s+Cr9V2HB3EzlDZ5n0qb8h5SJtGUEkjrphZYAglEiXZ6kiXdmk0H/h/uu/w==} - wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -13093,10 +11844,6 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - xtend@2.2.0: - resolution: {integrity: sha512-SLt5uylT+4aoXxXuwtQp5ZnMMzhDb1Xkg4pEqc00WUJCQifPfV9Ub1VrNhp9kXkrjZD2I2Hl8WnjP37jzZLPZw==} - engines: {node: '>=0.4'} - xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -13158,9 +11905,6 @@ packages: peerDependencies: zod: ^3.25.0 || ^4.0.0 - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zod@4.1.13: resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} @@ -14343,10 +13087,6 @@ snapshots: '@chevrotain/utils@11.0.3': {} - '@choojs/findup@0.2.1': - dependencies: - commander: 2.20.3 - '@clack/core@1.0.1': dependencies: picocolors: 1.1.1 @@ -15760,11 +14500,6 @@ snapshots: '@esbuild/win32-x64@0.25.10': optional: true - '@eslint-community/eslint-utils@4.9.0(eslint@9.36.0(jiti@2.6.1))': - dependencies: - eslint: 9.36.0(jiti@2.6.1) - eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@2.6.1))': dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -15780,16 +14515,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.3.1': {} - '@eslint/config-helpers@0.4.2': dependencies: '@eslint/core': 0.17.0 - '@eslint/core@0.15.2': - dependencies: - '@types/json-schema': 7.0.15 - '@eslint/core@0.17.0': dependencies: '@types/json-schema': 7.0.15 @@ -15808,17 +14537,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.36.0': {} - '@eslint/js@9.39.1': {} '@eslint/object-schema@2.1.7': {} - '@eslint/plugin-kit@0.3.5': - dependencies: - '@eslint/core': 0.15.2 - levn: 0.4.1 - '@eslint/plugin-kit@0.4.1': dependencies: '@eslint/core': 0.17.0 @@ -16119,45 +14841,6 @@ snapshots: '@leichtgewicht/ip-codec@2.0.5': {} - '@mapbox/geojson-rewind@0.5.2': - dependencies: - get-stream: 6.0.1 - minimist: 1.2.8 - - '@mapbox/geojson-types@1.0.2': {} - - '@mapbox/jsonlint-lines-primitives@2.0.2': {} - - '@mapbox/mapbox-gl-supported@1.5.0(mapbox-gl@1.13.3)': - dependencies: - mapbox-gl: 1.13.3 - - '@mapbox/point-geometry@0.1.0': {} - - '@mapbox/tiny-sdf@1.2.5': {} - - '@mapbox/tiny-sdf@2.1.0': {} - - '@mapbox/unitbezier@0.0.0': {} - - '@mapbox/unitbezier@0.0.1': {} - - '@mapbox/vector-tile@1.3.1': - dependencies: - '@mapbox/point-geometry': 0.1.0 - - '@mapbox/whoots-js@3.1.0': {} - - '@maplibre/maplibre-gl-style-spec@20.4.0': - dependencies: - '@mapbox/jsonlint-lines-primitives': 2.0.2 - '@mapbox/unitbezier': 0.0.1 - json-stringify-pretty-compact: 4.0.0 - minimist: 1.2.8 - quickselect: 2.0.0 - rw: 1.3.3 - tinyqueue: 3.0.0 - '@mdx-js/mdx@3.1.1': dependencies: '@types/estree': 1.0.8 @@ -17104,63 +15787,6 @@ snapshots: dependencies: playwright: 1.58.1 - '@plotly/d3-sankey-circular@0.33.1': - dependencies: - d3-array: 1.2.4 - d3-collection: 1.0.7 - d3-shape: 1.3.7 - elementary-circuits-directed-graph: 1.3.1 - - '@plotly/d3-sankey@0.7.2': - dependencies: - d3-array: 1.2.4 - d3-collection: 1.0.7 - d3-shape: 1.3.7 - - '@plotly/d3@3.8.2': {} - - '@plotly/mapbox-gl@1.13.4(mapbox-gl@1.13.3)': - dependencies: - '@mapbox/geojson-rewind': 0.5.2 - '@mapbox/geojson-types': 1.0.2 - '@mapbox/jsonlint-lines-primitives': 2.0.2 - '@mapbox/mapbox-gl-supported': 1.5.0(mapbox-gl@1.13.3) - '@mapbox/point-geometry': 0.1.0 - '@mapbox/tiny-sdf': 1.2.5 - '@mapbox/unitbezier': 0.0.0 - '@mapbox/vector-tile': 1.3.1 - '@mapbox/whoots-js': 3.1.0 - csscolorparser: 1.0.3 - earcut: 2.2.4 - geojson-vt: 3.2.1 - gl-matrix: 3.4.4 - grid-index: 1.1.0 - murmurhash-js: 1.0.0 - pbf: 3.3.0 - potpack: 1.0.2 - quickselect: 2.0.0 - rw: 1.3.3 - supercluster: 7.1.5 - tinyqueue: 2.0.3 - vt-pbf: 3.1.3 - transitivePeerDependencies: - - mapbox-gl - - '@plotly/point-cluster@3.1.9': - dependencies: - array-bounds: 1.0.1 - binary-search-bounds: 2.0.5 - clamp: 1.0.1 - defined: 1.0.1 - dtype: 2.0.0 - flatten-vertex-data: 1.0.2 - is-obj: 1.0.1 - math-log2: 1.0.1 - parse-rect: 1.2.0 - pick-by-alias: 1.2.0 - - '@plotly/regl@2.1.2': {} - '@pnpm/config.env-replace@1.1.0': {} '@pnpm/network.ca-file@1.0.2': @@ -17879,18 +16505,6 @@ snapshots: '@radix-ui/rect@1.1.1': {} - '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.2)(react@19.2.0)(redux@5.0.1))(react@19.2.0)': - dependencies: - '@standard-schema/spec': 1.1.0 - '@standard-schema/utils': 0.3.0 - immer: 11.1.4 - redux: 5.0.1 - redux-thunk: 3.1.0(redux@5.0.1) - reselect: 5.1.1 - optionalDependencies: - react: 19.2.0 - react-redux: 9.2.0(@types/react@19.2.2)(react@19.2.0)(redux@5.0.1) - '@release-it/conventional-changelog@10.0.4(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.2.1)(release-it@19.2.0(@types/node@24.7.2)(magicast@0.3.5))': dependencies: '@conventional-changelog/git-client': 2.5.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.2.1) @@ -18109,50 +16723,19 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@shikijs/core@3.15.0': - dependencies: - '@shikijs/types': 3.15.0 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - hast-util-to-html: 9.0.5 - - '@shikijs/engine-javascript@3.15.0': - dependencies: - '@shikijs/types': 3.15.0 - '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 4.3.6 - - '@shikijs/engine-oniguruma@3.15.0': - dependencies: - '@shikijs/types': 3.15.0 - '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/engine-oniguruma@3.20.0': dependencies: '@shikijs/types': 3.20.0 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.15.0': - dependencies: - '@shikijs/types': 3.15.0 - '@shikijs/langs@3.20.0': dependencies: '@shikijs/types': 3.20.0 - '@shikijs/themes@3.15.0': - dependencies: - '@shikijs/types': 3.15.0 - '@shikijs/themes@3.20.0': dependencies: '@shikijs/types': 3.20.0 - '@shikijs/types@3.15.0': - dependencies: - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - '@shikijs/types@3.20.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 @@ -18223,8 +16806,6 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@standard-schema/utils@0.3.0': {} - '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -18326,16 +16907,6 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@tailwindcss/node@4.1.17': - dependencies: - '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.18.3 - jiti: 2.6.1 - lightningcss: 1.30.2 - magic-string: 0.30.21 - source-map-js: 1.2.1 - tailwindcss: 4.1.17 - '@tailwindcss/node@4.1.18': dependencies: '@jridgewell/remapping': 2.3.5 @@ -18346,93 +16917,42 @@ snapshots: source-map-js: 1.2.1 tailwindcss: 4.1.18 - '@tailwindcss/oxide-android-arm64@4.1.17': - optional: true - '@tailwindcss/oxide-android-arm64@4.1.18': optional: true - '@tailwindcss/oxide-darwin-arm64@4.1.17': - optional: true - '@tailwindcss/oxide-darwin-arm64@4.1.18': optional: true - '@tailwindcss/oxide-darwin-x64@4.1.17': - optional: true - '@tailwindcss/oxide-darwin-x64@4.1.18': optional: true - '@tailwindcss/oxide-freebsd-x64@4.1.17': - optional: true - '@tailwindcss/oxide-freebsd-x64@4.1.18': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': - optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': - optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.1.17': - optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.1.18': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.1.17': - optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.1.18': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.1.17': - optional: true - '@tailwindcss/oxide-linux-x64-musl@4.1.18': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.1.17': - optional: true - '@tailwindcss/oxide-wasm32-wasi@4.1.18': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': - optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.1.17': - optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.1.18': optional: true - '@tailwindcss/oxide@4.1.17': - optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.17 - '@tailwindcss/oxide-darwin-arm64': 4.1.17 - '@tailwindcss/oxide-darwin-x64': 4.1.17 - '@tailwindcss/oxide-freebsd-x64': 4.1.17 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.17 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.17 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.17 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.17 - '@tailwindcss/oxide-linux-x64-musl': 4.1.17 - '@tailwindcss/oxide-wasm32-wasi': 4.1.17 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.17 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.17 - '@tailwindcss/oxide@4.1.18': optionalDependencies: '@tailwindcss/oxide-android-arm64': 4.1.18 @@ -18448,14 +16968,6 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 - '@tailwindcss/postcss@4.1.17': - dependencies: - '@alloc/quick-lru': 5.2.0 - '@tailwindcss/node': 4.1.17 - '@tailwindcss/oxide': 4.1.17 - postcss: 8.5.6 - tailwindcss: 4.1.17 - '@tailwindcss/postcss@4.1.18': dependencies: '@alloc/quick-lru': 5.2.0 @@ -18464,152 +16976,14 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.18 - '@tanstack/history@1.133.19': {} - - '@tanstack/react-router-devtools@1.133.22(@tanstack/react-router@1.133.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.133.20)(@types/node@24.6.0)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.12)(terser@5.44.1)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.2)': - dependencies: - '@tanstack/react-router': 1.133.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@tanstack/router-devtools-core': 1.133.22(@tanstack/router-core@1.133.20)(@types/node@24.6.0)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(solid-js@1.9.12)(terser@5.44.1)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.2) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - vite: 7.2.4(@types/node@24.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2) - transitivePeerDependencies: - - '@tanstack/router-core' - - '@types/node' - - csstype - - jiti - - less - - lightningcss - - sass - - sass-embedded - - solid-js - - stylus - - sugarss - - terser - - tiny-invariant - - tsx - - yaml - - '@tanstack/react-router@1.133.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@tanstack/history': 1.133.19 - '@tanstack/react-store': 0.7.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@tanstack/router-core': 1.133.20 - isbot: 5.1.39 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - tiny-invariant: 1.3.3 - tiny-warning: 1.0.3 - - '@tanstack/react-store@0.7.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@tanstack/store': 0.7.7 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - use-sync-external-store: 1.6.0(react@19.2.0) - '@tanstack/react-table@8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@tanstack/table-core': 8.21.3 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - '@tanstack/router-cli@1.133.20': - dependencies: - '@tanstack/router-generator': 1.133.20 - chokidar: 3.6.0 - yargs: 17.7.2 - transitivePeerDependencies: - - supports-color - - '@tanstack/router-core@1.133.20': - dependencies: - '@tanstack/history': 1.133.19 - '@tanstack/store': 0.7.7 - cookie-es: 2.0.1 - seroval: 1.5.2 - seroval-plugins: 1.5.2(seroval@1.5.2) - tiny-invariant: 1.3.3 - tiny-warning: 1.0.3 - - '@tanstack/router-devtools-core@1.133.22(@tanstack/router-core@1.133.20)(@types/node@24.6.0)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(solid-js@1.9.12)(terser@5.44.1)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.2)': - dependencies: - '@tanstack/router-core': 1.133.20 - clsx: 2.1.1 - goober: 2.1.18(csstype@3.2.3) - solid-js: 1.9.12 - tiny-invariant: 1.3.3 - vite: 7.2.4(@types/node@24.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2) - optionalDependencies: - csstype: 3.2.3 - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml - - '@tanstack/router-generator@1.133.20': - dependencies: - '@tanstack/router-core': 1.133.20 - '@tanstack/router-utils': 1.133.19 - '@tanstack/virtual-file-routes': 1.133.19 - prettier: 3.8.1 - recast: 0.23.11 - source-map: 0.7.6 - tsx: 4.20.6 - zod: 3.25.76 - transitivePeerDependencies: - - supports-color - - '@tanstack/router-plugin@1.133.22(@tanstack/react-router@1.133.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(rolldown-vite@7.1.14(@types/node@24.6.0)(esbuild@0.25.10)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2))(webpack@5.103.0(esbuild@0.25.10))': - dependencies: - '@babel/core': 7.28.5 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@tanstack/router-core': 1.133.20 - '@tanstack/router-generator': 1.133.20 - '@tanstack/router-utils': 1.133.19 - '@tanstack/virtual-file-routes': 1.133.19 - babel-dead-code-elimination: 1.0.12 - chokidar: 3.6.0 - unplugin: 2.3.11 - zod: 3.25.76 - optionalDependencies: - '@tanstack/react-router': 1.133.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - vite: rolldown-vite@7.1.14(@types/node@24.6.0)(esbuild@0.25.10)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2) - webpack: 5.103.0(esbuild@0.25.10) - transitivePeerDependencies: - - supports-color - - '@tanstack/router-utils@1.133.19': - dependencies: - '@babel/core': 7.28.5 - '@babel/generator': 7.29.1 - '@babel/parser': 7.29.0 - '@babel/preset-typescript': 7.28.5(@babel/core@7.28.5) - ansis: 4.2.0 - diff: 8.0.4 - pathe: 2.0.3 - tinyglobby: 0.2.15 - transitivePeerDependencies: - - supports-color - - '@tanstack/store@0.7.7': {} - '@tanstack/table-core@8.21.3': {} - '@tanstack/virtual-file-routes@1.133.19': {} - '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.27.1 @@ -18635,42 +17009,10 @@ snapshots: '@trysound/sax@0.2.0': {} - '@turf/area@7.3.5': + '@tybys/wasm-util@0.10.1': dependencies: - '@turf/helpers': 7.3.5 - '@turf/meta': 7.3.5 - '@types/geojson': 7946.0.16 tslib: 2.8.1 - - '@turf/bbox@7.3.5': - dependencies: - '@turf/helpers': 7.3.5 - '@turf/meta': 7.3.5 - '@types/geojson': 7946.0.16 - tslib: 2.8.1 - - '@turf/centroid@7.3.5': - dependencies: - '@turf/helpers': 7.3.5 - '@turf/meta': 7.3.5 - '@types/geojson': 7946.0.16 - tslib: 2.8.1 - - '@turf/helpers@7.3.5': - dependencies: - '@types/geojson': 7946.0.16 - tslib: 2.8.1 - - '@turf/meta@7.3.5': - dependencies: - '@turf/helpers': 7.3.5 - '@types/geojson': 7946.0.16 - tslib: 2.8.1 - - '@tybys/wasm-util@0.10.1': - dependencies: - tslib: 2.8.1 - optional: true + optional: true '@types/aria-query@5.0.4': {} @@ -18891,10 +17233,6 @@ snapshots: '@types/qs': 6.14.0 '@types/serve-static': 1.15.10 - '@types/geojson-vt@3.2.5': - dependencies: - '@types/geojson': 7946.0.16 - '@types/geojson@7946.0.16': {} '@types/gtag.js@0.0.12': {} @@ -18935,14 +17273,6 @@ snapshots: '@types/lodash@4.17.24': {} - '@types/mapbox__point-geometry@0.1.4': {} - - '@types/mapbox__vector-tile@1.3.4': - dependencies: - '@types/geojson': 7946.0.16 - '@types/mapbox__point-geometry': 0.1.4 - '@types/pbf': 3.0.5 - '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -19003,8 +17333,6 @@ snapshots: '@types/parse5@5.0.3': {} - '@types/pbf@3.0.5': {} - '@types/pg-pool@2.0.6': dependencies: '@types/pg': 8.16.0 @@ -19021,8 +17349,6 @@ snapshots: pg-protocol: 1.11.0 pg-types: 2.2.0 - '@types/plotly.js@3.0.10': {} - '@types/prismjs@1.26.5': {} '@types/qs@6.14.0': {} @@ -19037,11 +17363,6 @@ snapshots: dependencies: '@types/react': 19.2.7 - '@types/react-plotly.js@2.6.4': - dependencies: - '@types/plotly.js': 3.0.10 - '@types/react': 19.2.2 - '@types/react-router-config@5.0.11': dependencies: '@types/history': 4.7.11 @@ -19098,10 +17419,6 @@ snapshots: dependencies: '@types/node': 25.2.3 - '@types/supercluster@7.1.3': - dependencies: - '@types/geojson': 7946.0.16 - '@types/tedious@4.0.14': dependencies: '@types/node': 25.2.3 @@ -19113,8 +17430,6 @@ snapshots: '@types/unist@3.0.3': {} - '@types/use-sync-external-store@0.0.6': {} - '@types/validator@13.15.10': {} '@types/ws@8.18.1': @@ -19127,23 +17442,6 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.45.0 - '@typescript-eslint/type-utils': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.45.0 - eslint: 9.36.0(jiti@2.6.1) - graphemer: 1.4.0 - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -19160,18 +17458,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/scope-manager': 8.45.0 - '@typescript-eslint/types': 8.45.0 - '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.45.0 - debug: 4.4.3 - eslint: 9.36.0(jiti@2.6.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.49.0 @@ -19184,15 +17470,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.45.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) - '@typescript-eslint/types': 8.49.0 - debug: 4.4.3 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/project-service@8.49.0(typescript@5.9.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) @@ -19202,36 +17479,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.45.0': - dependencies: - '@typescript-eslint/types': 8.45.0 - '@typescript-eslint/visitor-keys': 8.45.0 - '@typescript-eslint/scope-manager@8.49.0': dependencies: '@typescript-eslint/types': 8.49.0 '@typescript-eslint/visitor-keys': 8.49.0 - '@typescript-eslint/tsconfig-utils@8.45.0(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.49.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 8.45.0 - '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) - debug: 4.4.3 - eslint: 9.36.0(jiti@2.6.1) - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/type-utils@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.49.0 @@ -19244,26 +17500,8 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.45.0': {} - '@typescript-eslint/types@8.49.0': {} - '@typescript-eslint/typescript-estree@8.45.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.45.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.45.0(typescript@5.9.3) - '@typescript-eslint/types': 8.45.0 - '@typescript-eslint/visitor-keys': 8.45.0 - debug: 4.4.3 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.4 - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/typescript-estree@8.49.0(typescript@5.9.3)': dependencies: '@typescript-eslint/project-service': 8.49.0(typescript@5.9.3) @@ -19279,17 +17517,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.45.0 - '@typescript-eslint/types': 8.45.0 - '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) - eslint: 9.36.0(jiti@2.6.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/utils@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) @@ -19301,11 +17528,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.45.0': - dependencies: - '@typescript-eslint/types': 8.45.0 - eslint-visitor-keys: 4.2.1 - '@typescript-eslint/visitor-keys@8.49.0': dependencies: '@typescript-eslint/types': 8.49.0 @@ -19315,18 +17537,6 @@ snapshots: '@vercel/oidc@3.0.5': {} - '@vitejs/plugin-react@5.0.4(rolldown-vite@7.1.14(@types/node@24.6.0)(esbuild@0.25.10)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2))': - dependencies: - '@babel/core': 7.28.5 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) - '@rolldown/pluginutils': 1.0.0-beta.38 - '@types/babel__core': 7.20.5 - react-refresh: 0.17.0 - vite: rolldown-vite@7.1.14(@types/node@24.6.0)(esbuild@0.25.10)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2) - transitivePeerDependencies: - - supports-color - '@vitejs/plugin-react@5.0.4(vite@7.2.4(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.5 @@ -19511,8 +17721,6 @@ snapshots: abbrev@4.0.0: optional: true - abs-svg-path@0.1.1: {} - accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -19534,8 +17742,6 @@ snapshots: dependencies: acorn: 8.15.0 - acorn@7.4.1: {} - acorn@8.15.0: {} address@1.2.2: {} @@ -19688,20 +17894,10 @@ snapshots: array-back@6.2.2: {} - array-bounds@1.0.1: {} - - array-find-index@1.0.2: {} - array-flatten@1.1.1: {} array-ify@1.0.0: {} - array-normalize@1.1.4: - dependencies: - array-bounds: 1.0.1 - - array-range@1.0.1: {} - array-union@2.1.0: {} assertion-error@2.0.1: {} @@ -19716,10 +17912,6 @@ snapshots: dependencies: tslib: 2.8.1 - ast-types@0.16.1: - dependencies: - tslib: 2.8.1 - astral-regex@2.0.0: {} astring@1.9.0: {} @@ -19732,16 +17924,6 @@ snapshots: dependencies: immediate: 3.3.0 - autoprefixer@10.4.21(postcss@8.5.6): - dependencies: - browserslist: 4.28.1 - caniuse-lite: 1.0.30001760 - fraction.js: 4.3.7 - normalize-range: 0.1.2 - picocolors: 1.1.1 - postcss: 8.5.6 - postcss-value-parser: 4.2.0 - autoprefixer@10.4.23(postcss@8.5.6): dependencies: browserslist: 4.28.1 @@ -19755,15 +17937,6 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - babel-dead-code-elimination@1.0.12: - dependencies: - '@babel/core': 7.28.5 - '@babel/parser': 7.29.0 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - transitivePeerDependencies: - - supports-color - babel-loader@9.2.1(@babel/core@7.28.5)(webpack@5.103.0): dependencies: '@babel/core': 7.28.5 @@ -19807,8 +17980,6 @@ snapshots: balanced-match@4.0.4: {} - base64-arraybuffer@1.0.2: {} - base64-js@1.5.1: {} baseline-browser-mapping@2.9.7: {} @@ -19841,19 +18012,8 @@ snapshots: binary-extensions@2.3.0: {} - binary-search-bounds@2.0.5: {} - birpc@4.0.0: {} - bit-twiddle@1.0.2: {} - - bitmap-sdf@1.0.4: {} - - bl@2.2.1: - dependencies: - readable-stream: 2.3.8 - safe-buffer: 5.2.1 - bl@4.1.0: dependencies: buffer: 5.7.1 @@ -20069,10 +18229,6 @@ snapshots: caniuse-lite@1.0.30001760: {} - canvas-fit@1.5.0: - dependencies: - element-size: 1.1.1 - ccount@2.0.1: {} chai@5.3.3: @@ -20188,8 +18344,6 @@ snapshots: cjs-module-lexer@1.4.3: {} - clamp@1.0.1: {} - class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -20257,46 +18411,12 @@ snapshots: collapse-white-space@2.1.0: {} - color-alpha@1.0.4: - dependencies: - color-parse: 1.4.3 - color-convert@2.0.1: dependencies: color-name: 1.1.4 - color-id@1.1.0: - dependencies: - clamp: 1.0.1 - color-name@1.1.4: {} - color-normalize@1.5.0: - dependencies: - clamp: 1.0.1 - color-rgba: 2.4.0 - dtype: 2.0.0 - - color-parse@1.4.3: - dependencies: - color-name: 1.1.4 - - color-parse@2.0.0: - dependencies: - color-name: 1.1.4 - - color-rgba@2.4.0: - dependencies: - color-parse: 1.4.3 - color-space: 2.3.2 - - color-rgba@3.0.0: - dependencies: - color-parse: 2.0.0 - color-space: 2.3.2 - - color-space@2.3.2: {} - color-support@1.1.3: {} colord@2.9.3: {} @@ -20364,13 +18484,6 @@ snapshots: concat-map@0.0.1: {} - concat-stream@1.6.2: - dependencies: - buffer-from: 1.1.2 - inherits: 2.0.4 - readable-stream: 2.3.8 - typedarray: 0.0.6 - concat-stream@2.0.0: dependencies: buffer-from: 1.1.2 @@ -20476,8 +18589,6 @@ snapshots: convert-source-map@2.0.0: {} - cookie-es@2.0.1: {} - cookie-signature@1.0.7: {} cookie@0.7.2: {} @@ -20535,8 +18646,6 @@ snapshots: optionalDependencies: typescript: 5.9.3 - country-regex@1.1.0: {} - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -20556,28 +18665,6 @@ snapshots: dependencies: postcss: 8.5.6 - css-font-size-keywords@1.0.0: {} - - css-font-stretch-keywords@1.0.1: {} - - css-font-style-keywords@1.0.1: {} - - css-font-weight-keywords@1.0.0: {} - - css-font@1.2.0: - dependencies: - css-font-size-keywords: 1.0.0 - css-font-stretch-keywords: 1.0.1 - css-font-style-keywords: 1.0.1 - css-font-weight-keywords: 1.0.0 - css-global-keywords: 1.0.1 - css-system-font-keywords: 1.0.0 - pick-by-alias: 1.2.0 - string-split-by: 1.0.0 - unquote: 1.1.1 - - css-global-keywords@1.0.1: {} - css-has-pseudo@7.0.3(postcss@8.5.6): dependencies: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.1) @@ -20635,8 +18722,6 @@ snapshots: css-selector-parser@3.3.0: {} - css-system-font-keywords@1.0.0: {} - css-tree@2.2.1: dependencies: mdn-data: 2.0.28 @@ -20654,8 +18739,6 @@ snapshots: css-what@6.2.2: {} - csscolorparser@1.0.3: {} - cssdb@8.5.2: {} cssesc@3.0.0: {} @@ -20741,8 +18824,6 @@ snapshots: cytoscape@3.33.1: {} - d3-array@1.2.4: {} - d3-array@2.12.1: dependencies: internmap: 1.0.1 @@ -20765,8 +18846,6 @@ snapshots: dependencies: d3-path: 3.1.0 - d3-collection@1.0.7: {} - d3-color@3.1.0: {} d3-contour@4.0.2: @@ -20777,8 +18856,6 @@ snapshots: dependencies: delaunator: 5.0.1 - d3-dispatch@1.0.6: {} - d3-dispatch@3.0.1: {} d3-drag@3.0.0: @@ -20798,40 +18875,18 @@ snapshots: dependencies: d3-dsv: 3.0.1 - d3-force@1.2.1: - dependencies: - d3-collection: 1.0.7 - d3-dispatch: 1.0.6 - d3-quadtree: 1.0.7 - d3-timer: 1.0.10 - d3-force@3.0.0: dependencies: d3-dispatch: 3.0.1 d3-quadtree: 3.0.1 d3-timer: 3.0.1 - d3-format@1.4.5: {} - d3-format@3.1.0: {} - d3-geo-projection@2.9.0: - dependencies: - commander: 2.20.3 - d3-array: 1.2.4 - d3-geo: 1.12.1 - resolve: 1.22.10 - - d3-geo@1.12.1: - dependencies: - d3-array: 1.2.4 - d3-geo@3.1.1: dependencies: d3-array: 3.2.4 - d3-hierarchy@1.1.9: {} - d3-hierarchy@3.1.2: {} d3-interpolate@3.0.1: @@ -20844,8 +18899,6 @@ snapshots: d3-polygon@3.0.1: {} - d3-quadtree@1.0.7: {} - d3-quadtree@3.0.1: {} d3-random@3.0.1: {} @@ -20878,22 +18931,14 @@ snapshots: dependencies: d3-path: 3.1.0 - d3-time-format@2.3.0: - dependencies: - d3-time: 1.1.0 - d3-time-format@4.1.0: dependencies: d3-time: 3.1.0 - d3-time@1.1.0: {} - d3-time@3.1.0: dependencies: d3-array: 3.2.4 - d3-timer@1.0.10: {} - d3-timer@3.0.1: {} d3-transition@3.0.1(d3-selection@3.0.0): @@ -20946,11 +18991,6 @@ snapshots: d3-transition: 3.0.1(d3-selection@3.0.0) d3-zoom: 3.0.0 - d@1.0.2: - dependencies: - es5-ext: 0.10.64 - type: 2.7.3 - dagre-d3-es@7.0.13: dependencies: d3: 7.9.0 @@ -20979,10 +19019,6 @@ snapshots: dependencies: ms: 2.0.0 - debug@3.2.7: - dependencies: - ms: 2.1.3 - debug@4.4.3: dependencies: ms: 2.1.3 @@ -21038,8 +19074,6 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - defined@1.0.1: {} - defu@6.1.4: {} degenerator@5.0.1: @@ -21062,8 +19096,6 @@ snapshots: destroy@1.2.0: {} - detect-kerning@2.1.2: {} - detect-libc@2.1.2: {} detect-node-es@1.1.0: {} @@ -21081,8 +19113,6 @@ snapshots: dependencies: dequal: 2.0.3 - diff@8.0.4: {} - dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -21188,11 +19218,6 @@ snapshots: dottie@2.0.6: {} - draw-svg-path@1.0.0: - dependencies: - abs-svg-path: 0.1.1 - normalize-svg-path: 0.1.0 - drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(pg@8.18.0): optionalDependencies: '@opentelemetry/api': 1.9.0 @@ -21203,29 +19228,14 @@ snapshots: optionalDependencies: oxc-resolver: 11.19.1 - dtype@2.0.0: {} - dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 es-errors: 1.3.0 gopd: 1.2.0 - dup@1.0.0: {} - duplexer@0.1.2: {} - duplexify@3.7.1: - dependencies: - end-of-stream: 1.4.5 - inherits: 2.0.4 - readable-stream: 2.3.8 - stream-shift: 1.0.3 - - earcut@2.2.4: {} - - earcut@3.0.2: {} - eastasianwidth@0.2.0: {} ecdsa-sig-formatter@1.0.11: @@ -21250,12 +19260,6 @@ snapshots: electron-to-chromium@1.5.267: {} - element-size@1.1.1: {} - - elementary-circuits-directed-graph@1.3.1: - dependencies: - strongly-connected-components: 1.0.1 - embla-carousel-react@8.6.0(react@19.2.0): dependencies: embla-carousel: 8.6.0 @@ -21294,6 +19298,7 @@ snapshots: end-of-stream@1.4.5: dependencies: once: 1.4.0 + optional: true enhanced-resolve@5.18.3: dependencies: @@ -21326,33 +19331,6 @@ snapshots: dependencies: es-errors: 1.3.0 - es-toolkit@1.46.0: {} - - es5-ext@0.10.64: - dependencies: - es6-iterator: 2.0.3 - es6-symbol: 3.1.4 - esniff: 2.0.1 - next-tick: 1.1.0 - - es6-iterator@2.0.3: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - es6-symbol: 3.1.4 - - es6-symbol@3.1.4: - dependencies: - d: 1.0.2 - ext: 1.7.0 - - es6-weak-map@2.0.3: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - es6-iterator: 2.0.3 - es6-symbol: 3.1.4 - esast-util-from-estree@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 @@ -21416,10 +19394,6 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-plugin-react-hooks@5.2.0(eslint@9.36.0(jiti@2.6.1)): - dependencies: - eslint: 9.36.0(jiti@2.6.1) - eslint-plugin-react-hooks@7.0.1(eslint@9.39.1(jiti@2.6.1)): dependencies: '@babel/core': 7.28.5 @@ -21431,10 +19405,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.4.22(eslint@9.36.0(jiti@2.6.1)): - dependencies: - eslint: 9.36.0(jiti@2.6.1) - eslint-plugin-react-refresh@0.4.24(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -21453,48 +19423,6 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.36.0(jiti@2.6.1): - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.1)) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.1 - '@eslint/config-helpers': 0.3.1 - '@eslint/core': 0.15.2 - '@eslint/eslintrc': 3.3.3 - '@eslint/js': 9.36.0 - '@eslint/plugin-kit': 0.3.5 - '@humanfs/node': 0.16.7 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - esquery: 1.6.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - optionalDependencies: - jiti: 2.6.1 - transitivePeerDependencies: - - supports-color - eslint@9.39.1(jiti@2.6.1): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) @@ -21536,13 +19464,6 @@ snapshots: transitivePeerDependencies: - supports-color - esniff@2.0.1: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - event-emitter: 0.3.5 - type: 2.7.3 - espree@10.4.0: dependencies: acorn: 8.15.0 @@ -21613,11 +19534,6 @@ snapshots: '@types/node': 25.2.3 require-like: 0.1.2 - event-emitter@0.3.5: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - eventemitter3@4.0.7: {} eventemitter3@5.0.1: {} @@ -21696,21 +19612,12 @@ snapshots: exsolve@1.0.8: {} - ext@1.7.0: - dependencies: - type: 2.7.3 - extend-shallow@2.0.1: dependencies: is-extendable: 0.1.1 extend@3.0.2: {} - falafel@2.2.5: - dependencies: - acorn: 7.4.1 - isarray: 2.0.5 - fast-content-type-parse@3.0.0: {} fast-deep-equal@3.1.3: {} @@ -21725,10 +19632,6 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-isnumeric@1.1.4: - dependencies: - is-string-blank: 1.0.1 - fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} @@ -21841,20 +19744,8 @@ snapshots: flatted@3.3.3: {} - flatten-vertex-data@1.0.2: - dependencies: - dtype: 2.0.0 - follow-redirects@1.15.11: {} - font-atlas@2.1.0: - dependencies: - css-font: 1.2.0 - - font-measure@1.2.2: - dependencies: - css-font: 1.2.0 - for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -21882,17 +19773,10 @@ snapshots: forwarded@0.2.0: {} - fraction.js@4.3.7: {} - fraction.js@5.3.4: {} fresh@0.5.2: {} - from2@2.3.0: - dependencies: - inherits: 2.0.4 - readable-stream: 2.3.8 - fs-constants@1.0.0: optional: true @@ -21966,14 +19850,8 @@ snapshots: gensync@1.0.0-beta.2: {} - geojson-vt@3.2.1: {} - - geojson-vt@4.0.2: {} - get-caller-file@2.0.5: {} - get-canvas-context@1.0.2: {} - get-east-asian-width@1.4.0: {} get-intrinsic@1.3.0: @@ -22048,40 +19926,6 @@ snapshots: github-slugger@1.5.0: {} - gl-mat4@1.2.0: {} - - gl-matrix@3.4.4: {} - - gl-text@1.4.0: - dependencies: - bit-twiddle: 1.0.2 - color-normalize: 1.5.0 - css-font: 1.2.0 - detect-kerning: 2.1.2 - es6-weak-map: 2.0.3 - flatten-vertex-data: 1.0.2 - font-atlas: 2.1.0 - font-measure: 1.2.2 - gl-util: 3.1.3 - is-plain-obj: 1.1.0 - object-assign: 4.1.1 - parse-rect: 1.2.0 - parse-unit: 1.0.1 - pick-by-alias: 1.2.0 - regl: 2.1.1 - to-px: 1.0.1 - typedarray-pool: 1.2.0 - - gl-util@3.1.3: - dependencies: - is-browser: 2.1.0 - is-firefox: 1.0.3 - is-plain-obj: 1.1.0 - number-is-integer: 1.0.1 - object-assign: 4.1.1 - pick-by-alias: 1.2.0 - weak-map: 1.0.8 - glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -22128,16 +19972,8 @@ snapshots: dependencies: ini: 2.0.0 - global-prefix@4.0.0: - dependencies: - ini: 4.1.3 - kind-of: 6.0.3 - which: 4.0.0 - globals@14.0.0: {} - globals@16.4.0: {} - globals@16.5.0: {} globby@11.1.0: @@ -22159,92 +19995,6 @@ snapshots: globrex@0.1.2: {} - glsl-inject-defines@1.0.3: - dependencies: - glsl-token-inject-block: 1.1.0 - glsl-token-string: 1.0.1 - glsl-tokenizer: 2.1.5 - - glsl-resolve@0.0.1: - dependencies: - resolve: 0.6.3 - xtend: 2.2.0 - - glsl-token-assignments@2.0.2: {} - - glsl-token-defines@1.0.0: - dependencies: - glsl-tokenizer: 2.1.5 - - glsl-token-depth@1.1.2: {} - - glsl-token-descope@1.0.2: - dependencies: - glsl-token-assignments: 2.0.2 - glsl-token-depth: 1.1.2 - glsl-token-properties: 1.0.1 - glsl-token-scope: 1.1.2 - - glsl-token-inject-block@1.1.0: {} - - glsl-token-properties@1.0.1: {} - - glsl-token-scope@1.1.2: {} - - glsl-token-string@1.0.1: {} - - glsl-token-whitespace-trim@1.0.0: {} - - glsl-tokenizer@2.1.5: - dependencies: - through2: 0.6.5 - - glslify-bundle@5.1.1: - dependencies: - glsl-inject-defines: 1.0.3 - glsl-token-defines: 1.0.0 - glsl-token-depth: 1.1.2 - glsl-token-descope: 1.0.2 - glsl-token-scope: 1.1.2 - glsl-token-string: 1.0.1 - glsl-token-whitespace-trim: 1.0.0 - glsl-tokenizer: 2.1.5 - murmurhash-js: 1.0.0 - shallow-copy: 0.0.1 - - glslify-deps@1.3.2: - dependencies: - '@choojs/findup': 0.2.1 - events: 3.3.0 - glsl-resolve: 0.0.1 - glsl-tokenizer: 2.1.5 - graceful-fs: 4.2.11 - inherits: 2.0.4 - map-limit: 0.0.1 - resolve: 1.22.10 - - glslify@7.1.1: - dependencies: - bl: 2.2.1 - concat-stream: 1.6.2 - duplexify: 3.7.1 - falafel: 2.2.5 - from2: 2.3.0 - glsl-resolve: 0.0.1 - glsl-token-whitespace-trim: 1.0.0 - glslify-bundle: 5.1.1 - glslify-deps: 1.3.2 - minimist: 1.2.8 - resolve: 1.22.10 - stack-trace: 0.0.9 - static-eval: 2.1.1 - through2: 2.0.5 - xtend: 4.0.2 - - goober@2.1.18(csstype@3.2.3): - dependencies: - csstype: 3.2.3 - google-auth-library@10.5.0: dependencies: base64-js: 1.5.1 @@ -22296,8 +20046,6 @@ snapshots: graceful-fs@4.2.11: {} - graphemer@1.4.0: {} - gray-matter@4.0.3: dependencies: js-yaml: 3.14.2 @@ -22305,8 +20053,6 @@ snapshots: section-matter: 1.0.0 strip-bom-string: 1.0.0 - grid-index@1.1.0: {} - gtoken@8.0.0: dependencies: gaxios: 7.1.3 @@ -22333,14 +20079,6 @@ snapshots: has-flag@4.0.0: {} - has-hover@1.0.1: - dependencies: - is-browser: 2.1.0 - - has-passive-events@1.0.0: - dependencies: - is-browser: 2.1.0 - has-property-descriptors@1.0.2: dependencies: es-define-property: 1.0.1 @@ -22823,10 +20561,6 @@ snapshots: immediate@3.3.0: {} - immer@10.2.0: {} - - immer@11.1.4: {} - import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -22863,8 +20597,6 @@ snapshots: ini@4.1.1: {} - ini@4.1.3: {} - ini@6.0.0: {} inline-style-parser@0.2.7: {} @@ -22913,8 +20645,6 @@ snapshots: dependencies: binary-extensions: 2.3.0 - is-browser@2.1.0: {} - is-buffer@2.0.5: {} is-callable@1.2.7: {} @@ -22937,10 +20667,6 @@ snapshots: is-extglob@2.1.1: {} - is-finite@1.1.0: {} - - is-firefox@1.0.3: {} - is-fullwidth-code-point@3.0.0: {} is-fullwidth-code-point@4.0.0: {} @@ -22966,8 +20692,6 @@ snapshots: is-interactive@2.0.0: {} - is-mobile@4.0.0: {} - is-network-error@1.3.0: {} is-npm@6.1.0: {} @@ -22980,8 +20704,6 @@ snapshots: is-path-inside@3.0.3: {} - is-plain-obj@1.1.0: {} - is-plain-obj@2.1.0: {} is-plain-obj@3.0.0: {} @@ -23006,10 +20728,6 @@ snapshots: is-stream@4.0.1: {} - is-string-blank@1.0.1: {} - - is-svg-path@1.0.2: {} - is-text-path@2.0.0: dependencies: text-extensions: 2.4.0 @@ -23038,12 +20756,8 @@ snapshots: isarray@2.0.5: {} - isbot@5.1.39: {} - isexe@2.0.0: {} - isexe@3.1.5: {} - isexe@4.0.0: {} isobject@3.0.1: {} @@ -23204,8 +20918,6 @@ snapshots: json-stringify-nice@1.1.4: {} - json-stringify-pretty-compact@4.0.0: {} - json5@2.2.3: {} jsonata@2.1.0: @@ -23243,10 +20955,6 @@ snapshots: dependencies: commander: 8.3.0 - kdbush@3.0.0: {} - - kdbush@4.0.2: {} - keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -23481,10 +21189,6 @@ snapshots: lru-cache@7.18.3: {} - lucide-react@0.546.0(react@19.2.0): - dependencies: - react: 19.2.0 - lucide-react@0.554.0(react@19.2.0): dependencies: react: 19.2.0 @@ -23529,64 +21233,6 @@ snapshots: - supports-color optional: true - map-limit@0.0.1: - dependencies: - once: 1.3.3 - - mapbox-gl@1.13.3: - dependencies: - '@mapbox/geojson-rewind': 0.5.2 - '@mapbox/geojson-types': 1.0.2 - '@mapbox/jsonlint-lines-primitives': 2.0.2 - '@mapbox/mapbox-gl-supported': 1.5.0(mapbox-gl@1.13.3) - '@mapbox/point-geometry': 0.1.0 - '@mapbox/tiny-sdf': 1.2.5 - '@mapbox/unitbezier': 0.0.0 - '@mapbox/vector-tile': 1.3.1 - '@mapbox/whoots-js': 3.1.0 - csscolorparser: 1.0.3 - earcut: 2.2.4 - geojson-vt: 3.2.1 - gl-matrix: 3.4.4 - grid-index: 1.1.0 - murmurhash-js: 1.0.0 - pbf: 3.3.0 - potpack: 1.0.2 - quickselect: 2.0.0 - rw: 1.3.3 - supercluster: 7.1.5 - tinyqueue: 2.0.3 - vt-pbf: 3.1.3 - - maplibre-gl@4.7.1: - dependencies: - '@mapbox/geojson-rewind': 0.5.2 - '@mapbox/jsonlint-lines-primitives': 2.0.2 - '@mapbox/point-geometry': 0.1.0 - '@mapbox/tiny-sdf': 2.1.0 - '@mapbox/unitbezier': 0.0.1 - '@mapbox/vector-tile': 1.3.1 - '@mapbox/whoots-js': 3.1.0 - '@maplibre/maplibre-gl-style-spec': 20.4.0 - '@types/geojson': 7946.0.16 - '@types/geojson-vt': 3.2.5 - '@types/mapbox__point-geometry': 0.1.4 - '@types/mapbox__vector-tile': 1.3.4 - '@types/pbf': 3.0.5 - '@types/supercluster': 7.1.3 - earcut: 3.0.2 - geojson-vt: 4.0.2 - gl-matrix: 3.4.4 - global-prefix: 4.0.0 - kdbush: 4.0.2 - murmurhash-js: 1.0.0 - pbf: 3.3.0 - potpack: 2.1.0 - quickselect: 3.0.0 - supercluster: 8.0.1 - tinyqueue: 3.0.0 - vt-pbf: 3.1.3 - mark.js@8.11.1: {} markdown-extensions@2.0.0: {} @@ -23612,8 +21258,6 @@ snapshots: math-intrinsics@1.1.0: {} - math-log2@1.0.1: {} - mdast-util-directive@3.1.0: dependencies: '@types/mdast': 4.0.4 @@ -24275,20 +21919,6 @@ snapshots: moment@2.30.1: {} - mouse-change@1.4.0: - dependencies: - mouse-event: 1.0.5 - - mouse-event-offset@3.0.2: {} - - mouse-event@1.0.5: {} - - mouse-wheel@1.2.0: - dependencies: - right-now: 1.0.0 - signum: 1.0.0 - to-px: 1.0.1 - mri@1.2.0: {} mrmime@2.0.1: {} @@ -24302,8 +21932,6 @@ snapshots: dns-packet: 5.6.1 thunky: 1.1.0 - murmurhash-js@1.0.0: {} - mute-stream@2.0.0: {} nanoid@3.3.11: {} @@ -24311,18 +21939,8 @@ snapshots: napi-build-utils@2.0.0: optional: true - native-promise-only@0.8.1: {} - natural-compare@1.4.0: {} - needle@2.9.1: - dependencies: - debug: 3.2.7 - iconv-lite: 0.4.24 - sax: 1.4.3 - transitivePeerDependencies: - - supports-color - negotiator@0.6.3: {} negotiator@0.6.4: {} @@ -24343,8 +21961,6 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - next-tick@1.1.0: {} - no-case@3.0.4: dependencies: lower-case: 2.0.2 @@ -24421,14 +22037,6 @@ snapshots: normalize-path@3.0.0: {} - normalize-range@0.1.2: {} - - normalize-svg-path@0.1.0: {} - - normalize-svg-path@1.1.0: - dependencies: - svg-arc-to-cubic-bezier: 3.2.0 - normalize-url@8.1.0: {} normalize-url@8.1.1: {} @@ -24475,10 +22083,6 @@ snapshots: schema-utils: 3.3.0 webpack: 5.103.0 - number-is-integer@1.0.1: - dependencies: - is-finite: 1.1.0 - nypm@0.6.2: dependencies: citty: 0.1.6 @@ -24517,15 +22121,12 @@ snapshots: dependencies: ee-first: 1.1.1 - on-headers@1.1.0: {} - - once@1.3.3: - dependencies: - wrappy: 1.0.2 + on-headers@1.1.0: {} once@1.4.0: dependencies: wrappy: 1.0.2 + optional: true onetime@5.1.2: dependencies: @@ -24539,14 +22140,6 @@ snapshots: dependencies: mimic-function: 5.0.1 - oniguruma-parser@0.12.2: {} - - oniguruma-to-es@4.3.6: - dependencies: - oniguruma-parser: 0.12.2 - regex: 6.1.0 - regex-recursion: 6.0.2 - open@10.2.0: dependencies: default-browser: 5.4.0 @@ -24694,8 +22287,6 @@ snapshots: dependencies: callsites: 3.1.0 - parenthesis@3.1.8: {} - parse-conflict-json@5.0.1: dependencies: json-parse-even-better-errors: 5.0.0 @@ -24725,14 +22316,6 @@ snapshots: dependencies: protocols: 2.0.2 - parse-rect@1.2.0: - dependencies: - pick-by-alias: 1.2.0 - - parse-svg-path@0.1.2: {} - - parse-unit@1.0.1: {} - parse-url@9.2.0: dependencies: '@types/parse-path': 7.1.0 @@ -24798,15 +22381,8 @@ snapshots: pathval@2.0.1: {} - pbf@3.3.0: - dependencies: - ieee754: 1.2.1 - resolve-protobuf-schema: 2.1.0 - perfect-debounce@2.0.0: {} - performance-now@2.1.0: {} - pg-cloudflare@1.3.0: optional: true @@ -24842,8 +22418,6 @@ snapshots: dependencies: split2: 4.2.0 - pick-by-alias@1.2.0: {} - picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -24876,64 +22450,6 @@ snapshots: optionalDependencies: fsevents: 2.3.2 - plotly.js@3.5.0(mapbox-gl@1.13.3): - dependencies: - '@plotly/d3': 3.8.2 - '@plotly/d3-sankey': 0.7.2 - '@plotly/d3-sankey-circular': 0.33.1 - '@plotly/mapbox-gl': 1.13.4(mapbox-gl@1.13.3) - '@plotly/regl': 2.1.2 - '@turf/area': 7.3.5 - '@turf/bbox': 7.3.5 - '@turf/centroid': 7.3.5 - base64-arraybuffer: 1.0.2 - canvas-fit: 1.5.0 - color-alpha: 1.0.4 - color-normalize: 1.5.0 - color-parse: 2.0.0 - color-rgba: 3.0.0 - country-regex: 1.1.0 - d3-force: 1.2.1 - d3-format: 1.4.5 - d3-geo: 1.12.1 - d3-geo-projection: 2.9.0 - d3-hierarchy: 1.1.9 - d3-interpolate: 3.0.1 - d3-time: 1.1.0 - d3-time-format: 2.3.0 - fast-isnumeric: 1.1.4 - gl-mat4: 1.2.0 - gl-text: 1.4.0 - has-hover: 1.0.1 - has-passive-events: 1.0.0 - is-mobile: 4.0.0 - maplibre-gl: 4.7.1 - mouse-change: 1.4.0 - mouse-event-offset: 3.0.2 - mouse-wheel: 1.2.0 - native-promise-only: 0.8.1 - parse-svg-path: 0.1.2 - point-in-polygon: 1.1.0 - polybooljs: 1.2.2 - probe-image-size: 7.2.3 - regl-error2d: 2.0.12 - regl-line2d: 3.1.3 - regl-scatter2d: 3.4.0 - regl-splom: 1.0.14 - strongly-connected-components: 1.0.1 - superscript-text: 1.0.0 - svg-path-sdf: 1.1.3 - tinycolor2: 1.6.0 - to-px: 1.0.1 - topojson-client: 3.1.0 - webgl-context: 2.2.0 - world-calendars: 1.0.4 - transitivePeerDependencies: - - mapbox-gl - - supports-color - - point-in-polygon@1.1.0: {} - points-on-curve@0.2.0: {} points-on-path@0.2.1: @@ -24941,8 +22457,6 @@ snapshots: path-data-parser: 0.1.0 points-on-curve: 0.2.0 - polybooljs@1.2.2: {} - possible-typed-array-names@1.1.0: {} postcss-attribute-case-insensitive@7.0.1(postcss@8.5.6): @@ -25394,10 +22908,6 @@ snapshots: dependencies: xtend: 4.0.2 - potpack@1.0.2: {} - - potpack@2.1.0: {} - prebuild-install@7.1.3: dependencies: detect-libc: 2.1.2 @@ -25439,14 +22949,6 @@ snapshots: prismjs@1.30.0: {} - probe-image-size@7.2.3: - dependencies: - lodash.merge: 4.6.2 - needle: 2.9.1 - stream-parser: 0.3.1 - transitivePeerDependencies: - - supports-color - proc-log@6.1.0: {} process-nextick-args@2.0.1: {} @@ -25492,8 +22994,6 @@ snapshots: '@types/node': 25.2.3 long: 5.3.2 - protocol-buffers-schema@3.6.1: {} - protocols@2.0.2: {} proxy-addr@2.0.7: @@ -25552,14 +23052,6 @@ snapshots: quick-lru@5.1.1: {} - quickselect@2.0.0: {} - - quickselect@3.0.0: {} - - raf@3.4.1: - dependencies: - performance-now: 2.1.0 - randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -25633,21 +23125,6 @@ snapshots: react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.0)' webpack: 5.103.0 - react-plotly.js@2.6.0(plotly.js@3.5.0(mapbox-gl@1.13.3))(react@19.2.0): - dependencies: - plotly.js: 3.5.0(mapbox-gl@1.13.3) - prop-types: 15.8.1 - react: 19.2.0 - - react-redux@9.2.0(@types/react@19.2.2)(react@19.2.0)(redux@5.0.1): - dependencies: - '@types/use-sync-external-store': 0.0.6 - react: 19.2.0 - use-sync-external-store: 1.6.0(react@19.2.0) - optionalDependencies: - '@types/react': 19.2.2 - redux: 5.0.1 - react-refresh@0.17.0: {} react-refresh@0.18.0: {} @@ -25740,13 +23217,6 @@ snapshots: json-parse-even-better-errors: 5.0.0 npm-normalize-package-bin: 5.0.0 - readable-stream@1.0.34: - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 0.0.1 - string_decoder: 0.10.31 - readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -25769,14 +23239,6 @@ snapshots: readdirp@5.0.0: {} - recast@0.23.11: - dependencies: - ast-types: 0.16.1 - esprima: 4.0.1 - source-map: 0.6.1 - tiny-invariant: 1.3.3 - tslib: 2.8.1 - recharts-scale@0.4.5: dependencies: decimal.js-light: 2.5.1 @@ -25794,26 +23256,6 @@ snapshots: tiny-invariant: 1.3.3 victory-vendor: 36.9.2 - recharts@3.4.1(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react-is@18.3.1)(react@19.2.0)(redux@5.0.1): - dependencies: - '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.2)(react@19.2.0)(redux@5.0.1))(react@19.2.0) - clsx: 2.1.1 - decimal.js-light: 2.5.1 - es-toolkit: 1.46.0 - eventemitter3: 5.0.1 - immer: 10.2.0 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - react-is: 18.3.1 - react-redux: 9.2.0(@types/react@19.2.2)(react@19.2.0)(redux@5.0.1) - reselect: 5.1.1 - tiny-invariant: 1.3.3 - use-sync-external-store: 1.6.0(react@19.2.0) - victory-vendor: 37.3.6 - transitivePeerDependencies: - - '@types/react' - - redux - recma-build-jsx@1.0.0: dependencies: '@types/estree': 1.0.8 @@ -25843,12 +23285,6 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 - redux-thunk@3.1.0(redux@5.0.1): - dependencies: - redux: 5.0.1 - - redux@5.0.1: {} - reflect-metadata@0.2.2: {} regenerate-unicode-properties@10.2.2: @@ -25857,16 +23293,6 @@ snapshots: regenerate@1.4.2: {} - regex-recursion@6.0.2: - dependencies: - regex-utilities: 2.3.0 - - regex-utilities@2.3.0: {} - - regex@6.1.0: - dependencies: - regex-utilities: 2.3.0 - regexpu-core@6.4.0: dependencies: regenerate: 1.4.2 @@ -25890,56 +23316,6 @@ snapshots: dependencies: jsesc: 3.1.0 - regl-error2d@2.0.12: - dependencies: - array-bounds: 1.0.1 - color-normalize: 1.5.0 - flatten-vertex-data: 1.0.2 - object-assign: 4.1.1 - pick-by-alias: 1.2.0 - to-float32: 1.1.0 - update-diff: 1.1.0 - - regl-line2d@3.1.3: - dependencies: - array-bounds: 1.0.1 - array-find-index: 1.0.2 - array-normalize: 1.1.4 - color-normalize: 1.5.0 - earcut: 2.2.4 - es6-weak-map: 2.0.3 - flatten-vertex-data: 1.0.2 - object-assign: 4.1.1 - parse-rect: 1.2.0 - pick-by-alias: 1.2.0 - to-float32: 1.1.0 - - regl-scatter2d@3.4.0: - dependencies: - '@plotly/point-cluster': 3.1.9 - array-bounds: 1.0.1 - color-id: 1.1.0 - color-normalize: 1.5.0 - flatten-vertex-data: 1.0.2 - glslify: 7.1.1 - parse-rect: 1.2.0 - pick-by-alias: 1.2.0 - to-float32: 1.1.0 - update-diff: 1.1.0 - - regl-splom@1.0.14: - dependencies: - array-bounds: 1.0.1 - array-range: 1.0.1 - color-alpha: 1.0.4 - flatten-vertex-data: 1.0.2 - parse-rect: 1.2.0 - pick-by-alias: 1.2.0 - raf: 3.4.1 - regl-scatter2d: 3.4.0 - - regl@2.1.1: {} - rehype-minify-whitespace@6.0.2: dependencies: '@types/hast': 3.0.4 @@ -26102,8 +23478,6 @@ snapshots: requires-port@1.0.0: {} - reselect@5.1.1: {} - resolve-alpn@1.2.1: {} resolve-from@4.0.0: {} @@ -26114,12 +23488,6 @@ snapshots: resolve-pkg-maps@1.0.0: {} - resolve-protobuf-schema@2.1.0: - dependencies: - protocol-buffers-schema: 3.6.1 - - resolve@0.6.3: {} - resolve@1.22.10: dependencies: is-core-module: 2.16.1 @@ -26147,8 +23515,6 @@ snapshots: rfdc@1.4.1: {} - right-now@1.0.0: {} - rimraf@5.0.10: dependencies: glob: 10.5.0 @@ -26190,24 +23556,6 @@ snapshots: tsx: 4.20.6 yaml: 2.8.2 - rolldown-vite@7.1.14(@types/node@24.6.0)(esbuild@0.25.10)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2): - dependencies: - '@oxc-project/runtime': 0.92.0 - fdir: 6.5.0(picomatch@4.0.3) - lightningcss: 1.30.2 - picomatch: 4.0.3 - postcss: 8.5.6 - rolldown: 1.0.0-beta.41 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 24.6.0 - esbuild: 0.25.10 - fsevents: 2.3.3 - jiti: 2.6.1 - terser: 5.44.1 - tsx: 4.20.6 - yaml: 2.8.2 - rolldown-vite@7.1.14(@types/node@25.2.3)(esbuild@0.25.10)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2): dependencies: '@oxc-project/runtime': 0.92.0 @@ -26465,12 +23813,6 @@ snapshots: dependencies: randombytes: 2.1.0 - seroval-plugins@1.5.2(seroval@1.5.2): - dependencies: - seroval: 1.5.2 - - seroval@1.5.2: {} - serve-handler@6.1.6: dependencies: bytes: 3.0.0 @@ -26525,8 +23867,6 @@ snapshots: dependencies: kind-of: 6.0.3 - shallow-copy@0.0.1: {} - shallowequal@1.1.0: {} shebang-command@2.0.0: @@ -26537,17 +23877,6 @@ snapshots: shell-quote@1.8.3: {} - shiki@3.15.0: - dependencies: - '@shikijs/core': 3.15.0 - '@shikijs/engine-javascript': 3.15.0 - '@shikijs/engine-oniguruma': 3.15.0 - '@shikijs/langs': 3.15.0 - '@shikijs/themes': 3.15.0 - '@shikijs/types': 3.15.0 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -26582,8 +23911,6 @@ snapshots: signal-exit@4.1.0: {} - signum@1.0.0: {} - simple-concat@1.0.1: optional: true @@ -26663,12 +23990,6 @@ snapshots: ip-address: 10.1.0 smart-buffer: 4.2.0 - solid-js@1.9.12: - dependencies: - csstype: 3.2.3 - seroval: 1.5.2 - seroval-plugins: 1.5.2(seroval@1.5.2) - sonner@2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: react: 19.2.0 @@ -26743,14 +24064,8 @@ snapshots: dependencies: minipass: 7.1.2 - stack-trace@0.0.9: {} - stackback@0.0.2: {} - static-eval@2.1.1: - dependencies: - escodegen: 2.1.0 - statuses@1.5.0: {} statuses@2.0.1: {} @@ -26761,20 +24076,8 @@ snapshots: stdin-discarder@0.2.2: {} - stream-parser@0.3.1: - dependencies: - debug: 2.6.9 - transitivePeerDependencies: - - supports-color - - stream-shift@1.0.3: {} - string-argv@0.3.2: {} - string-split-by@1.0.0: - dependencies: - parenthesis: 3.1.8 - string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -26798,8 +24101,6 @@ snapshots: get-east-asian-width: 1.4.0 strip-ansi: 7.1.2 - string_decoder@0.10.31: {} - string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 @@ -26843,8 +24144,6 @@ snapshots: dependencies: js-tokens: 9.0.1 - strongly-connected-components@1.0.1: {} - style-to-js@1.1.21: dependencies: style-to-object: 1.0.14 @@ -26861,16 +24160,6 @@ snapshots: stylis@4.3.6: {} - supercluster@7.1.5: - dependencies: - kdbush: 3.0.0 - - supercluster@8.0.1: - dependencies: - kdbush: 4.0.2 - - superscript-text@1.0.0: {} - supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -26881,25 +24170,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svg-arc-to-cubic-bezier@3.2.0: {} - svg-parser@2.0.4: {} - svg-path-bounds@1.0.2: - dependencies: - abs-svg-path: 0.1.1 - is-svg-path: 1.0.2 - normalize-svg-path: 1.1.0 - parse-svg-path: 0.1.2 - - svg-path-sdf@1.1.3: - dependencies: - bitmap-sdf: 1.0.4 - draw-svg-path: 1.0.0 - is-svg-path: 1.0.2 - parse-svg-path: 0.1.2 - svg-path-bounds: 1.0.2 - svgo@3.3.2: dependencies: '@trysound/sax': 0.2.0 @@ -26931,8 +24203,6 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - tailwind-merge@3.3.1: {} - tailwind-merge@3.4.0: {} tailwindcss-animate@1.0.7(tailwindcss@4.1.17): @@ -26970,18 +24240,6 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - terser-webpack-plugin@5.3.16(esbuild@0.25.10)(webpack@5.103.0(esbuild@0.25.10)): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - jest-worker: 27.5.1 - schema-utils: 4.3.3 - serialize-javascript: 6.0.2 - terser: 5.44.1 - webpack: 5.103.0(esbuild@0.25.10) - optionalDependencies: - esbuild: 0.25.10 - optional: true - terser-webpack-plugin@5.3.16(webpack@5.103.0): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -27012,16 +24270,6 @@ snapshots: throttleit@2.1.0: {} - through2@0.6.5: - dependencies: - readable-stream: 1.0.34 - xtend: 4.0.2 - - through2@2.0.5: - dependencies: - readable-stream: 2.3.8 - xtend: 4.0.2 - through@2.3.8: {} thunky@1.1.0: {} @@ -27032,8 +24280,6 @@ snapshots: tinybench@2.9.0: {} - tinycolor2@1.6.0: {} - tinyexec@0.3.2: {} tinyexec@1.0.2: {} @@ -27045,10 +24291,6 @@ snapshots: tinypool@1.1.1: {} - tinyqueue@2.0.3: {} - - tinyqueue@3.0.0: {} - tinyrainbow@2.0.0: {} tinyspy@4.0.4: {} @@ -27065,12 +24307,6 @@ snapshots: safe-buffer: 5.2.1 typed-array-buffer: 1.0.3 - to-float32@1.1.0: {} - - to-px@1.0.1: - dependencies: - parse-unit: 1.0.1 - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -27082,10 +24318,6 @@ snapshots: toidentifier@1.0.1: {} - topojson-client@3.1.0: - dependencies: - commander: 2.20.3 - toposort-class@1.0.1: {} totalist@3.0.1: {} @@ -27223,19 +24455,12 @@ snapshots: mime-types: 3.0.2 optional: true - type@2.7.3: {} - typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 es-errors: 1.3.0 is-typed-array: 1.1.15 - typedarray-pool@1.2.0: - dependencies: - bit-twiddle: 1.0.2 - dup: 1.0.0 - typedarray-to-buffer@3.1.5: dependencies: is-typedarray: 1.0.0 @@ -27282,17 +24507,6 @@ snapshots: - babel-plugin-macros - supports-color - typescript-eslint@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3): - dependencies: - '@typescript-eslint/eslint-plugin': 8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.36.0(jiti@2.6.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - typescript-eslint@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) @@ -27434,15 +24648,6 @@ snapshots: unpipe@1.0.0: {} - unplugin@2.3.11: - dependencies: - '@jridgewell/remapping': 2.3.5 - acorn: 8.15.0 - picomatch: 4.0.3 - webpack-virtual-modules: 0.6.2 - - unquote@1.1.1: {} - unrun@0.2.28: dependencies: rolldown: 1.0.0-rc.5 @@ -27453,8 +24658,6 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - update-diff@1.1.0: {} - update-notifier@6.0.2: dependencies: boxen: 7.1.1 @@ -27590,23 +24793,6 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - victory-vendor@37.3.6: - dependencies: - '@types/d3-array': 3.2.2 - '@types/d3-ease': 3.0.2 - '@types/d3-interpolate': 3.0.4 - '@types/d3-scale': 4.0.9 - '@types/d3-shape': 3.1.7 - '@types/d3-time': 3.0.4 - '@types/d3-timer': 3.0.2 - d3-array: 3.2.4 - d3-ease: 3.0.1 - d3-interpolate: 3.0.1 - d3-scale: 4.0.2 - d3-shape: 3.2.0 - d3-time: 3.1.0 - d3-timer: 3.0.1 - vite-node@3.2.4(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2): dependencies: cac: 6.7.14 @@ -27656,23 +24842,6 @@ snapshots: tsx: 4.20.6 yaml: 2.8.2 - vite@7.2.4(@types/node@24.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2): - dependencies: - esbuild: 0.25.10 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.52.4 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 24.6.0 - fsevents: 2.3.3 - jiti: 2.6.1 - lightningcss: 1.30.2 - terser: 5.44.1 - tsx: 4.20.6 - yaml: 2.8.2 - vite@7.2.4(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2): dependencies: esbuild: 0.25.10 @@ -27750,12 +24919,6 @@ snapshots: vscode-uri@3.0.8: {} - vt-pbf@3.1.3: - dependencies: - '@mapbox/point-geometry': 0.1.0 - '@mapbox/vector-tile': 1.3.1 - pbf: 3.3.0 - w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 @@ -27771,18 +24934,12 @@ snapshots: dependencies: minimalistic-assert: 1.0.1 - weak-map@1.0.8: {} - web-namespaces@1.1.4: {} web-namespaces@2.0.1: {} web-streams-polyfill@3.3.3: {} - webgl-context@2.2.0: - dependencies: - get-canvas-context: 1.0.2 - webidl-conversions@3.0.1: {} webidl-conversions@8.0.0: {} @@ -27868,8 +25025,6 @@ snapshots: webpack-sources@3.3.3: {} - webpack-virtual-modules@0.6.2: {} - webpack@5.103.0: dependencies: '@types/eslint-scope': 3.7.7 @@ -27902,39 +25057,6 @@ snapshots: - esbuild - uglify-js - webpack@5.103.0(esbuild@0.25.10): - dependencies: - '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/wasm-edit': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.15.0 - acorn-import-phases: 1.0.4(acorn@8.15.0) - browserslist: 4.28.1 - chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.3 - es-module-lexer: 1.7.0 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.1 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 4.3.3 - tapable: 2.3.0 - terser-webpack-plugin: 5.3.16(esbuild@0.25.10)(webpack@5.103.0(esbuild@0.25.10)) - watchpack: 2.4.4 - webpack-sources: 3.3.3 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - optional: true - webpackbar@6.0.1(webpack@5.103.0): dependencies: ansi-escapes: 4.3.2 @@ -27985,10 +25107,6 @@ snapshots: dependencies: isexe: 2.0.0 - which@4.0.0: - dependencies: - isexe: 3.1.5 - which@6.0.1: dependencies: isexe: 4.0.0 @@ -28024,10 +25142,6 @@ snapshots: wordwrapjs@5.1.1: {} - world-calendars@1.0.4: - dependencies: - object-assign: 4.1.1 - wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -28052,7 +25166,8 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.2 - wrappy@1.0.2: {} + wrappy@1.0.2: + optional: true write-file-atomic@3.0.3: dependencies: @@ -28087,8 +25202,6 @@ snapshots: xmlchars@2.2.0: {} - xtend@2.2.0: {} - xtend@4.0.2: {} y18n@5.0.8: {} @@ -28137,8 +25250,6 @@ snapshots: dependencies: zod: 4.1.13 - zod@3.25.76: {} - zod@4.1.13: {} zod@4.3.6: {} From 13203ff808e69a4daf5b51840f624b18546080d5 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Thu, 30 Apr 2026 10:39:51 +0200 Subject: [PATCH 15/34] feat(appkit): translate UC structured format objects into printf strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DESCRIBE TABLE EXTENDED ... AS JSON for a metric view stores the YAML 1.1 `format` attribute as a structured object, not a printf string: { "currency": { "decimal_places": { "type": "EXACT", "places": 2 }, "currency_code": "USD" } } { "percent": { "decimal_places": { "places": 1 } } } { "number": { "decimal_places": { "places": 0 } } } Phase 5's extractor only accepted strings, so the bundle's `format` field was never populated even after a successful sync. `formatValue` and `toD3Format` then fell back to default localized formatting. New extractFormatString() handles both shapes: - String: passthrough (legacy / hand-authored bundles) - Object: dispatch on outer key (currency / percent / number) and translate to a printf string consumable by the existing format utilities. Currency code → symbol map covers USD ($), EUR (€), GBP (£), JPY/CNY (¥), INR (₹), BRL (R$). Unknown codes fall back to " " prefix so the information isn't lost — formatValue and d3-format render either way. Defaults: 2 decimal places + USD when the structured currency object omits those fields. Tolerates `decimal_places` as either a nested { places: n } object (canonical) or a bare number (legacy/short form). 9 new tests covering: USD currency, EUR with 0 places, unknown code fallback, percent with 1 place, percent with 0 places, number with comma grouping, unrecognized format object → undefined, currency defaults when fields missing, decimal_places as bare number. 50/50 type-gen tests pass; full backpressure (build, docs, check:fix, typecheck, test, knip) green. Co-authored-by: Isaac Signed-off-by: Atila Fassina --- .../src/type-generator/metric-registry.ts | 135 ++++++++++++++- .../tests/metric-registry.test.ts | 162 ++++++++++++++++++ 2 files changed, 296 insertions(+), 1 deletion(-) diff --git a/packages/appkit/src/type-generator/metric-registry.ts b/packages/appkit/src/type-generator/metric-registry.ts index dcfaa91be..fa9a2c3d1 100644 --- a/packages/appkit/src/type-generator/metric-registry.ts +++ b/packages/appkit/src/type-generator/metric-registry.ts @@ -357,7 +357,7 @@ export function extractMetricColumns(parsed: unknown): MetricColumnMetadata[] { "display_name", "displayName", ]); - const format = extractStringFromAny(obj, ["format", "format_spec"]); + const format = extractFormatString(obj); // Time-grain inference is type-driven, not YAML-attribute-driven. // Earlier versions of this code looked for a `time_grain` field on each @@ -410,6 +410,139 @@ function extractStringFromAny( return undefined; } +/** + * Read the column's `format` attribute from a DESCRIBE entry and return a + * printf-like format string suitable for `formatValue` and `toD3Format`. + * + * Tolerates two source shapes: + * + * 1. **Legacy / hand-authored** — `format: "$#,##0.00"` (already a printf + * string). Returned as-is. + * + * 2. **YAML 1.1 structured** — DESCRIBE TABLE EXTENDED ... AS JSON for a + * UC Metric View wraps the column's format type as the outer key: + * + * ``` + * { "currency": { "decimal_places": { "places": 2 }, "currency_code": "USD" } } + * { "percent": { "decimal_places": { "places": 1 } } } + * { "number": { "decimal_places": { "places": 0 } } } + * ``` + * + * Both shapes are checked at top-level (`obj.format` / `obj.format_spec`) + * and under `metadata.` for parity with extractStringFromAny. + * + * Unrecognized objects return undefined; downstream consumers fall back to + * default locale formatting. + */ +function extractFormatString(obj: Record): string | undefined { + for (const key of ["format", "format_spec"]) { + const direct = obj[key]; + const fromDirect = formatStringFromValue(direct); + if (fromDirect) return fromDirect; + + const meta = obj.metadata; + if (meta && typeof meta === "object" && !Array.isArray(meta)) { + const nested = (meta as Record)[key]; + const fromMeta = formatStringFromValue(nested); + if (fromMeta) return fromMeta; + } + } + return undefined; +} + +function formatStringFromValue(value: unknown): string | undefined { + if (typeof value === "string" && value.trim().length > 0) return value.trim(); + if (value && typeof value === "object" && !Array.isArray(value)) { + return translateStructuredFormat(value as Record); + } + return undefined; +} + +/** + * Translate the structured `format` object emitted by DESCRIBE TABLE EXTENDED + * AS JSON into a printf-like format string. Recognizes the three YAML 1.1 + * shapes; returns undefined for anything else. + */ +function translateStructuredFormat( + spec: Record, +): string | undefined { + if (spec.currency && typeof spec.currency === "object") { + return currencyFormatString(spec.currency as Record); + } + if (spec.percent && typeof spec.percent === "object") { + return percentFormatString(spec.percent as Record); + } + if (spec.number && typeof spec.number === "object") { + return numberFormatString(spec.number as Record); + } + return undefined; +} + +function currencyFormatString(c: Record): string { + const places = readDecimalPlaces(c) ?? 2; + const codeRaw = c.currency_code; + const code = + typeof codeRaw === "string" && codeRaw.trim().length > 0 + ? codeRaw.toUpperCase() + : "USD"; + const symbol = currencySymbol(code); + return `${symbol}#,##0${fractionalSuffix(places)}`; +} + +function percentFormatString(p: Record): string { + const places = readDecimalPlaces(p) ?? 0; + return `0${fractionalSuffix(places)}%`; +} + +function numberFormatString(n: Record): string { + const places = readDecimalPlaces(n) ?? 0; + return `#,##0${fractionalSuffix(places)}`; +} + +function fractionalSuffix(places: number): string { + return places > 0 ? `.${"0".repeat(places)}` : ""; +} + +function readDecimalPlaces(obj: Record): number | undefined { + const dp = obj.decimal_places; + if (typeof dp === "number" && Number.isFinite(dp) && dp >= 0) { + return Math.floor(dp); + } + if (dp && typeof dp === "object" && !Array.isArray(dp)) { + const places = (dp as Record).places; + if (typeof places === "number" && Number.isFinite(places) && places >= 0) { + return Math.floor(places); + } + } + return undefined; +} + +/** + * Map ISO currency codes to their conventional prefix symbol. Unknown codes + * fall back to the literal code + space (e.g., "AUD #,##0.00") so the value + * is never lost — `formatValue` and `toD3Format` will still render correctly, + * just without a single-character glyph. + */ +function currencySymbol(code: string): string { + switch (code) { + case "USD": + return "$"; + case "EUR": + return "€"; + case "GBP": + return "£"; + case "JPY": + case "CNY": + return "¥"; + case "INR": + return "₹"; + case "BRL": + return "R$"; + default: + return `${code} `; + } +} + /** * Infer the standard set of valid time grains for a dimension based on its * SQL data type. diff --git a/packages/appkit/src/type-generator/tests/metric-registry.test.ts b/packages/appkit/src/type-generator/tests/metric-registry.test.ts index bb354b435..419abde4d 100644 --- a/packages/appkit/src/type-generator/tests/metric-registry.test.ts +++ b/packages/appkit/src/type-generator/tests/metric-registry.test.ts @@ -549,6 +549,168 @@ describe("extractMetricColumns — Phase 5 semantic metadata", () => { }); expect(cols[0].format).toBe("$#,##0.00"); }); + + // ── Structured-format translation (UC YAML 1.1 → printf string) ──────── + // DESCRIBE TABLE EXTENDED ... AS JSON wraps the format type as the outer + // key: { currency: { ... } } / { percent: { ... } } / { number: { ... } }. + // The extractor translates these into printf strings consumable by + // formatValue / toD3Format. + test("translates structured currency format with USD", () => { + const cols = extractMetricColumns({ + columns: [ + { + name: "arr", + type: "DOUBLE", + is_measure: true, + metadata: { + format: { + currency: { + decimal_places: { type: "EXACT", places: 2 }, + currency_code: "USD", + }, + }, + }, + }, + ], + }); + expect(cols[0].format).toBe("$#,##0.00"); + }); + + test("translates structured currency format with EUR + 0 decimal places", () => { + const cols = extractMetricColumns({ + columns: [ + { + name: "ticket_price", + type: "DOUBLE", + is_measure: true, + metadata: { + format: { + currency: { + decimal_places: { places: 0 }, + currency_code: "EUR", + }, + }, + }, + }, + ], + }); + expect(cols[0].format).toBe("€#,##0"); + }); + + test("falls back to ISO code as literal prefix for unknown currencies", () => { + const cols = extractMetricColumns({ + columns: [ + { + name: "amount", + type: "DOUBLE", + is_measure: true, + metadata: { + format: { + currency: { + decimal_places: { places: 2 }, + currency_code: "AUD", + }, + }, + }, + }, + ], + }); + expect(cols[0].format).toBe("AUD #,##0.00"); + }); + + test("translates structured percent format with 1 decimal place", () => { + const cols = extractMetricColumns({ + columns: [ + { + name: "churn_rate", + type: "DECIMAL", + is_measure: true, + metadata: { + format: { + percent: { decimal_places: { places: 1 } }, + }, + }, + }, + ], + }); + expect(cols[0].format).toBe("0.0%"); + }); + + test("translates structured percent with 0 decimal places", () => { + const cols = extractMetricColumns({ + columns: [ + { + name: "rate", + type: "DECIMAL", + is_measure: true, + metadata: { format: { percent: { decimal_places: { places: 0 } } } }, + }, + ], + }); + expect(cols[0].format).toBe("0%"); + }); + + test("translates structured number format with comma grouping", () => { + const cols = extractMetricColumns({ + columns: [ + { + name: "active_accounts", + type: "BIGINT", + is_measure: true, + metadata: { + format: { number: { decimal_places: { places: 0 } } }, + }, + }, + ], + }); + expect(cols[0].format).toBe("#,##0"); + }); + + test("returns undefined for unrecognized structured format shapes", () => { + const cols = extractMetricColumns({ + columns: [ + { + name: "weirdo", + type: "DOUBLE", + is_measure: true, + metadata: { + format: { custom_thing: { whatever: 1 } }, + }, + }, + ], + }); + expect(cols[0].format).toBeUndefined(); + }); + + test("currency format defaults to USD + 2 places when fields are missing", () => { + const cols = extractMetricColumns({ + columns: [ + { + name: "amount", + type: "DOUBLE", + is_measure: true, + metadata: { format: { currency: {} } }, + }, + ], + }); + expect(cols[0].format).toBe("$#,##0.00"); + }); + + test("accepts decimal_places as a bare number (legacy shape)", () => { + const cols = extractMetricColumns({ + columns: [ + { + name: "amount", + type: "DOUBLE", + is_measure: true, + metadata: { + format: { currency: { decimal_places: 4, currency_code: "USD" } }, + }, + }, + ], + }); + expect(cols[0].format).toBe("$#,##0.0000"); + }); }); // ── Phase 5: metadata bundle generation ─────────────────────────────────── From cbadec7e139fcee3e26005ab2fb90bd515598a05 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Thu, 30 Apr 2026 10:42:52 +0200 Subject: [PATCH 16/34] feat(playground): theme-aware Plotly layout in /metrics route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plotly assumes a light background by default — text and gridlines stayed near-black even when the playground was in dark mode, leaving the chart nearly unreadable. Wire the chart layout to the theme via a small useIsDarkMode() hook that watches the `dark` class on (set by the existing ThemeSelector component). Backgrounds (paper + plot) are transparent so the Card's theme-aware bg shows through unchanged. Font / grid / axis colors swap between zinc-200/zinc-800 (dark) and zinc-900/zinc-200 (light) — close enough to the design tokens to feel native without plumbing CSS-var reads through Plotly's layout object. Co-authored-by: Isaac Signed-off-by: Atila Fassina --- .../client/src/routes/metrics.route.tsx | 70 ++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/apps/dev-playground/client/src/routes/metrics.route.tsx b/apps/dev-playground/client/src/routes/metrics.route.tsx index 6154549e8..32d66aa4f 100644 --- a/apps/dev-playground/client/src/routes/metrics.route.tsx +++ b/apps/dev-playground/client/src/routes/metrics.route.tsx @@ -84,6 +84,57 @@ type RevenueRow = { created_at: string; }; +/** + * Watch the dark-mode class on `` so Plotly's layout can swap palettes + * when the ThemeSelector flips. Plotly does not auto-detect themes — the + * defaults assume a light background, which clash with the playground's dark + * mode (white text-on-card becomes invisible on a near-black plot bg). + */ +function useIsDarkMode(): boolean { + const [isDark, setIsDark] = useState(() => + typeof document !== "undefined" + ? document.documentElement.classList.contains("dark") + : false, + ); + + useEffect(() => { + if (typeof document === "undefined") return; + const root = document.documentElement; + const update = () => setIsDark(root.classList.contains("dark")); + const observer = new MutationObserver(update); + observer.observe(root, { attributes: true, attributeFilter: ["class"] }); + update(); + return () => observer.disconnect(); + }, []); + + return isDark; +} + +/** + * Theme-aware color palette for Plotly. Backgrounds are transparent so the + * Card behind the plot shows through (the card's bg is already theme-aware). + * Foreground / grid / axis colors are hardcoded to neutral hex matching the + * AppKit tailwind palette — close enough to the design tokens to feel native + * without plumbing CSS-var reads at render time. + */ +function plotlyThemeColors(isDark: boolean) { + return isDark + ? { + paper: "transparent", + plot: "transparent", + font: "#e5e7eb", // zinc-200 + grid: "#27272a", // zinc-800 + axis: "#71717a", // zinc-500 + } + : { + paper: "transparent", + plot: "transparent", + font: "#18181b", // zinc-900 + grid: "#e4e4e7", // zinc-200 + axis: "#71717a", // zinc-500 + }; +} + function RevenueChart() { // Wrap args in `useMemo` so reference stability prevents infinite refetches. const args = useMemo( @@ -102,6 +153,8 @@ function RevenueChart() { ); const { data, metadata, loading, error } = useMetricView("revenue", args); + const isDark = useIsDarkMode(); + const colors = plotlyThemeColors(isDark); if (loading) { return ( @@ -178,16 +231,29 @@ function RevenueChart() { Date: Mon, 4 May 2026 11:56:45 +0200 Subject: [PATCH 17/34] fix(appkit-ui): harden useMetricView and trim client metric metadata Three review-followup fixes on the client side of the metric-view surface: - Defend against unstable args: serialize once per render via argsKey = JSON.stringify(args) and read fresh values through a ref. Inline args objects no longer cause an infinite refetch loop; consumers who memoize args see the same path with no extra cost. Docs tip is downgraded from required to optional. - Scrub raw warehouse / server error text in production. The SSE error path sets a generic React error in prod; dev mode keeps the passthrough so [TABLE_OR_VIEW_NOT_FOUND] etc. still surface for diagnosis. Full message stays in console.error for ops. - Drop source (UC FQN) and lane from the public MetricMetadata / MetricSemanticMetadata shape. Those are server-side concerns; the client bundle should carry only display-name / format / time-grain hints. Tests assert these fields are no longer reachable. Co-authored-by: Isaac --- docs/docs/plugins/analytics-metric-views.md | 4 +- .../src/format/__tests__/registry.test.ts | 21 +++---- packages/appkit-ui/src/format/types.ts | 9 +-- .../use-metric-view-metadata.test.ts | 16 ++--- .../hooks/__tests__/use-metric-view.test.ts | 48 +++++++++++++++ packages/appkit-ui/src/react/hooks/types.ts | 5 +- .../src/react/hooks/use-metric-view.ts | 59 ++++++++++++------- 7 files changed, 113 insertions(+), 49 deletions(-) diff --git a/docs/docs/plugins/analytics-metric-views.md b/docs/docs/plugins/analytics-metric-views.md index eb7fc73d3..4d7d2cd21 100644 --- a/docs/docs/plugins/analytics-metric-views.md +++ b/docs/docs/plugins/analytics-metric-views.md @@ -173,9 +173,9 @@ function RevenueChart() { } ``` -:::tip Memoize args +:::tip Memoize args (optional) -Wrap the `args` object in `useMemo` so reference stability prevents infinite refetches — the hook re-fires whenever the args reference changes, mirroring `useAnalyticsQuery`. +The hook deduplicates `args` by content (JSON-serialized), so re-rendering with a fresh-but-equivalent `args` object does not refetch. Wrapping `args` in `useMemo` is still recommended for very hot render paths to skip the per-render serialization, but it is no longer required for correctness. ::: ### Type-safe registration diff --git a/packages/appkit-ui/src/format/__tests__/registry.test.ts b/packages/appkit-ui/src/format/__tests__/registry.test.ts index 9696f094d..4c7d37024 100644 --- a/packages/appkit-ui/src/format/__tests__/registry.test.ts +++ b/packages/appkit-ui/src/format/__tests__/registry.test.ts @@ -13,8 +13,6 @@ afterEach(() => { const sampleBundle: MetricsMetadataBundle = { revenue: { - source: "demo.public.revenue", - lane: "sp", measures: { arr: { type: "DECIMAL(38,2)", @@ -27,8 +25,6 @@ const sampleBundle: MetricsMetadataBundle = { }, }, customer_metrics: { - source: "demo.public.customer_metrics", - lane: "obo", measures: { churn: { type: "DOUBLE", format: "0.0%" }, }, @@ -47,8 +43,10 @@ describe("registerMetricsMetadata + getMetricMetadata", () => { registerMetricsMetadata(sampleBundle); const metadata = getMetricMetadata("revenue"); expect(metadata).not.toBeNull(); - expect(metadata?.source).toBe("demo.public.revenue"); expect(metadata?.measures.arr.format).toBe("$#,##0.00"); + expect(metadata?.measures.arr.display_name).toBe( + "Annual Recurring Revenue", + ); }); test("returns null for an unregistered metric key", () => { @@ -69,8 +67,6 @@ describe("registerMetricsMetadata + getMetricMetadata", () => { const newBundle: MetricsMetadataBundle = { orders: { - source: "demo.public.orders", - lane: "sp", measures: { count: { type: "BIGINT" } }, dimensions: {}, }, @@ -94,9 +90,14 @@ describe("registerMetricsMetadata + getMetricMetadata", () => { expect(_getRegisteredBundleForTesting()).toBeNull(); }); - test("supports both SP and OBO lanes in the same bundle", () => { + test("returns metadata for any registered key regardless of execution lane", () => { + // Lane is a server-side concern (lives in metric.json) and is not part + // of the client-facing bundle. The hook returns metadata uniformly for + // both SP-lane and OBO-lane metrics. registerMetricsMetadata(sampleBundle); - expect(getMetricMetadata("revenue")?.lane).toBe("sp"); - expect(getMetricMetadata("customer_metrics")?.lane).toBe("obo"); + expect(getMetricMetadata("revenue")?.measures.arr.format).toBe("$#,##0.00"); + expect(getMetricMetadata("customer_metrics")?.measures.churn.format).toBe( + "0.0%", + ); }); }); diff --git a/packages/appkit-ui/src/format/types.ts b/packages/appkit-ui/src/format/types.ts index 809c12b1c..6a51d85c9 100644 --- a/packages/appkit-ui/src/format/types.ts +++ b/packages/appkit-ui/src/format/types.ts @@ -51,12 +51,13 @@ export interface ColumnMetadata { * One metric's complete semantic-metadata bundle. * * Top-level matches the shape in the build-time `metrics.metadata.json` file: - * `Record`. Each entry carries the FQN, the - * execution lane, and per-column metadata for measures and dimensions. + * `Record`. Each entry carries per-column metadata + * for measures and dimensions — display names, format specs, descriptions, + * time-grain hints. Server-side concerns (UC FQN, execution lane) live in + * `metric.json` and are deliberately NOT part of this artifact: it ships to + * the client. */ export interface MetricMetadata { - source: string; - lane: "sp" | "obo"; measures: Record; dimensions: Record; } diff --git a/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view-metadata.test.ts b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view-metadata.test.ts index c75bdb225..3e0fedf6a 100644 --- a/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view-metadata.test.ts +++ b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view-metadata.test.ts @@ -21,8 +21,6 @@ import { useMetricView } from "../use-metric-view"; const REVENUE_BUNDLE: MetricsMetadataBundle = { revenue: { - source: "appkit_demo.public.revenue_metrics", - lane: "sp", measures: { arr: { type: "DECIMAL(38,2)", @@ -45,8 +43,6 @@ const REVENUE_BUNDLE: MetricsMetadataBundle = { }, }, other_metric: { - source: "demo.public.other", - lane: "sp", measures: { count: { type: "BIGINT" } }, dimensions: {}, }, @@ -140,17 +136,19 @@ describe("useMetricView — Phase 5 metadata return field", () => { }, ); + // The `key as never` cast above narrows the metadata return type to + // `never`; assert on the runtime shape with a structural cast. const revenueMetadata = result.current.metadata as unknown as { - source: string; + measures: Record; } | null; rerender({ key: "other_metric" }); const otherMetadata = result.current.metadata as unknown as { - source: string; + measures: Record; } | null; expect(revenueMetadata).not.toBe(otherMetadata); - expect(revenueMetadata?.source).toBe("appkit_demo.public.revenue_metrics"); - expect(otherMetadata?.source).toBe("demo.public.other"); + expect(revenueMetadata?.measures).toHaveProperty("arr"); + expect(otherMetadata?.measures).toHaveProperty("count"); }); test("metadata is null when the metric key is not in the registered bundle", () => { @@ -197,8 +195,6 @@ describe("useMetricView — Phase 5 metadata return field", () => { // depends on. const newBundle: MetricsMetadataBundle = { revenue: { - source: "demo.public.new_revenue", - lane: "sp", measures: { arr: { type: "DECIMAL", format: "0.00" } }, dimensions: {}, }, diff --git a/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts index a2cafa841..aab51b5cd 100644 --- a/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts +++ b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts @@ -350,6 +350,54 @@ describe("useMetricView", () => { expect(capturedCallbacks.signal?.aborted).toBe(true); }); + test("does not refetch when re-rendered with structurally identical inline args", () => { + const { rerender } = renderHook( + ({ args }: { args: any }) => useMetricView("revenue", args), + { + initialProps: { + args: { + measures: ["arr"], + dimensions: ["region"], + filter: { member: "region", operator: "in", values: ["EMEA"] }, + }, + }, + }, + ); + expect(mockConnectSSE).toHaveBeenCalledTimes(1); + + // Fresh `args` reference with identical content — simulates a consumer + // that forgot to wrap args in useMemo. + rerender({ + args: { + measures: ["arr"], + dimensions: ["region"], + filter: { member: "region", operator: "in", values: ["EMEA"] }, + }, + }); + rerender({ + args: { + measures: ["arr"], + dimensions: ["region"], + filter: { member: "region", operator: "in", values: ["EMEA"] }, + }, + }); + + expect(mockConnectSSE).toHaveBeenCalledTimes(1); + }); + + test("refetches when args content actually changes", () => { + const { rerender } = renderHook( + ({ args }: { args: any }) => useMetricView("revenue", args), + { + initialProps: { args: { measures: ["arr"] } }, + }, + ); + expect(mockConnectSSE).toHaveBeenCalledTimes(1); + + rerender({ args: { measures: ["mrr"] } }); + expect(mockConnectSSE).toHaveBeenCalledTimes(2); + }); + test("rejects an empty metric key", () => { expect(() => // Cast to any so the runtime guard ("non-empty string") is what fails, diff --git a/packages/appkit-ui/src/react/hooks/types.ts b/packages/appkit-ui/src/react/hooks/types.ts index 9111c268c..2da830587 100644 --- a/packages/appkit-ui/src/react/hooks/types.ts +++ b/packages/appkit-ui/src/react/hooks/types.ts @@ -285,10 +285,11 @@ export interface MetricColumnMetadata { * by `useMetricView` in its `metadata` field — TypeScript narrows * `metadata.measures.` and `metadata.dimensions.` from the * registry's per-metric `metadata` augmentation when `K` is a registered key. + * + * Server-side concerns (UC FQN, execution lane) are deliberately NOT part of + * this shape — they live in `metric.json` and never reach the client bundle. */ export interface MetricSemanticMetadata { - source: string; - lane: "sp" | "obo"; measures: Record; dimensions: Record; } diff --git a/packages/appkit-ui/src/react/hooks/use-metric-view.ts b/packages/appkit-ui/src/react/hooks/use-metric-view.ts index f63f22522..b790ae2c9 100644 --- a/packages/appkit-ui/src/react/hooks/use-metric-view.ts +++ b/packages/appkit-ui/src/react/hooks/use-metric-view.ts @@ -82,26 +82,43 @@ export function useMetricView< [metricKey], ); + // Stable serialization key — defends against consumers passing inline + // `args` (new object every render) without `useMemo`. JSON.stringify runs + // once per render and is bounded by `maxParametersSize`; the payload memo + // (and the downstream effect) only re-fires when the request body actually + // changes by content. Without this, every render with fresh references + // would reset state and refetch, producing an infinite loop. + const argsKey = JSON.stringify(args); + + // Hold the latest `args` in a ref so the payload memo can read fresh + // values without listing each `args.*` field as a dep. The ref always + // matches the closed-over `argsKey`: when content changes, both update + // in the same render before the memo body runs. + const argsRef = useRef(args); + argsRef.current = args; + + // biome-ignore lint/correctness/useExhaustiveDependencies: argsKey is the trigger; args read via argsRef const payload = useMemo(() => { try { - const dimensions = args.dimensions ? [...args.dimensions] : undefined; + const a = argsRef.current; + const dimensions = a.dimensions ? [...a.dimensions] : undefined; const body: Record = { - measures: [...args.measures], + measures: [...a.measures], format, }; if (dimensions && dimensions.length > 0) { body.dimensions = dimensions; } - if (typeof args.timeGrain === "string" && args.timeGrain.length > 0) { - body.timeGrain = args.timeGrain; + if (typeof a.timeGrain === "string" && a.timeGrain.length > 0) { + body.timeGrain = a.timeGrain; } - if (args.filter !== undefined) { + if (a.filter !== undefined) { // Filter is a recursive AND/OR/Predicate tree; preserve structure // verbatim — the server validates and translates it into SQL. - body.filter = args.filter; + body.filter = a.filter; } - if (typeof args.limit === "number") { - body.limit = args.limit; + if (typeof a.limit === "number") { + body.limit = a.limit; } const serialized = JSON.stringify(body); const sizeInBytes = new Blob([serialized]).size; @@ -115,15 +132,7 @@ export function useMetricView< console.error("useMetricView: Failed to serialize request body", err); return null; } - }, [ - args.measures, - args.dimensions, - args.timeGrain, - args.filter, - args.limit, - format, - maxParametersSize, - ]); + }, [argsKey, format, maxParametersSize]); const start = useCallback(() => { if (payload === null) { @@ -169,13 +178,21 @@ export function useMetricView< } if (parsed.type === "error" || parsed.error || parsed.code) { - const errorMsg = + const rawMsg = parsed.error || parsed.message || "Unable to execute metric"; + // Defense-in-depth: do not echo raw warehouse / server error + // text (which can contain SQL fragments, FQNs, schema detail) to + // the user in production. Dev mode keeps the passthrough so + // developers can diagnose schema-not-found, auth-failed, etc. + // The full message is still logged via console.error for ops. + const userMsg = import.meta.env.DEV + ? rawMsg + : "Unable to execute metric"; setLoading(false); - setError(errorMsg); - if (parsed.code) { + setError(userMsg); + if (parsed.code || rawMsg !== userMsg) { console.error( - `[useMetricView] Code: ${parsed.code}, Message: ${errorMsg}`, + `[useMetricView] Code: ${parsed.code ?? "(none)"}, Message: ${rawMsg}`, ); } return; From dca98c470b32ef4368470f13a4b1800de1cd4f4a Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Mon, 4 May 2026 11:57:11 +0200 Subject: [PATCH 18/34] fix(appkit): server-side hardening for metric views (security + correctness) Bundles the server / CLI fixes from the same review pass so the metric-view surface is consistent with the appkit-ui changes that just landed. Security: - Fail-closed registry. POST /api/analytics/metric/:key returns 503 METRIC_REGISTRY_NOT_READY when the registered metric has no build-time measure list. Without this gate the validator falls open and arbitrary identifiers reach the warehouse, which both leaks schema and runs unintended SQL. - Generic public 400 for validation failures. validateMetricRequest now returns the offending field paths only (e.g. fields: filter.member); the full Zod issue list, including allowlist enumerations, stays in the AppKitError context for telemetry. - Drop source (UC FQN) and lane from the build-time metrics.metadata.json bundle. Server reads them from metric.json directly; the client bundle is now display-name / format / time-grain only. Correctness: - Reject empty { or: [] } at validation time; SQL builder renders an empty or to 1 = 0 as defense in depth so a future validator bypass cannot turn it into match-everything. - Op-vs-value type checks: contains/notContains require a string; range ops on a numeric dim require a numeric value. Surfaces 400 instead of letting buildMetricSql throw a 500. - canonicalizeFilter uses JSON.stringify on each value segment so ["a", "b"] no longer collides with ["a|string:b"] in the cache key. - Tighten timeGrain rejection: when the registry knows the metric's dims but none are time-typed, timeGrain is meaningless and is now rejected at validation time. Pure fall-open is reserved for the no-metadata-at-all path, which the route's fail-closed gate prevents in production. - Wrap OBO executor setup in the route's try/catch so asUser / resolveUserId throws (e.g. AuthenticationError on missing token) surface through the canonical error envelope. - syncMetrics now returns { schemas, failures }; the CLI surfaces failures (parse errors, zero-column extraction) as MetricSyncError so CI catches a silently-broken metric view instead of shipping an empty bundle entry. Tests updated to assert path-only public messages, the new fail-closed behavior, the cache-key non-collision, and the per-entry sync failure propagation. Snapshot regenerated. Co-authored-by: Isaac --- .../appkit/src/plugins/analytics/analytics.ts | 58 ++++-- .../appkit/src/plugins/analytics/metric.ts | 124 ++++++++++--- .../plugins/analytics/tests/metric.test.ts | 171 ++++++++++++++++-- packages/appkit/src/type-generator/index.ts | 35 +++- .../src/type-generator/metric-registry.ts | 95 +++++++--- .../metric-registry.test.ts.snap | 4 - .../tests/metric-registry.test.ts | 35 ++-- .../src/cli/commands/metric/sync/sync.test.ts | 43 ++++- .../src/cli/commands/metric/sync/sync.ts | 38 +++- .../src/cli/commands/type-generator.d.ts | 15 +- 10 files changed, 510 insertions(+), 108 deletions(-) diff --git a/packages/appkit/src/plugins/analytics/analytics.ts b/packages/appkit/src/plugins/analytics/analytics.ts index 2751d8434..096b34052 100644 --- a/packages/appkit/src/plugins/analytics/analytics.ts +++ b/packages/appkit/src/plugins/analytics/analytics.ts @@ -295,9 +295,50 @@ export class AnalyticsPlugin extends Plugin { return; } + // Fail-closed: if the build-time DESCRIBE never produced a measure list + // for this metric, the body validator falls open (no allowlist) and the + // SQL constructor would let arbitrary measure/dim references through to + // the warehouse. Refuse the request so an empty/missing + // `metrics.metadata.json` cannot become a schema-enumeration vector. + // The clear server-side fix is to (re-)run `pnpm exec appkit metric sync`. + if (registration.knownMeasures.length === 0) { + logger.warn( + req, + "Metric %s registered but build-time metadata is empty — refusing the request. Run `appkit metric sync` to populate metrics.metadata.json.", + key, + ); + res.status(503).json({ + error: "Metric registry not initialized", + code: "METRIC_REGISTRY_NOT_READY", + }); + return; + } + + // Single try/catch covering both body validation and executor setup — + // OBO lane's `asUser(req)` and `resolveUserId(req)` can throw on a + // missing/invalid `x-forwarded-access-token` (AuthenticationError). If + // they bubble up unwrapped, the route returns a malformed response + // outside the canonical error envelope. We compute the executor inside + // the same `try` so the auth error lands on the canonical 401 path. let request: ReturnType; + let executor: AnalyticsPlugin; + let executorKey: string; + let isAsUser: boolean; try { request = validateMetricRequest(registration, req.body ?? {}); + isAsUser = registration.lane === "obo"; + // OBO lane: dispatch via the existing asUser(req) Proxy — same pattern + // used by .obo.sql files in `_handleQueryRoute`. The Proxy threads the + // user's `x-forwarded-access-token` through every Databricks call so + // the warehouse executes the query under the end user's identity. + executor = isAsUser ? this.asUser(req) : this; + // OBO cache key: hash the user identity so the raw email/principal name + // never reaches the cache layer. SP cache key: literal "sp" — the cache + // is shared across every caller of the SP-lane metric. + executorKey = deriveMetricExecutorKey({ + lane: registration.lane, + userIdentity: isAsUser ? this.resolveUserId(req) : null, + }); } catch (err) { if (err instanceof AppKitError) { res.status(err.statusCode).json({ @@ -306,7 +347,9 @@ export class AnalyticsPlugin extends Plugin { }); return; } - // Validator only throws ValidationError, but be defensive. + // Validator throws ValidationError; asUser/resolveUserId throw + // AuthenticationError — both are AppKitError. This branch only fires + // for unexpected errors; keep generic to avoid leaking internals. res.status(400).json({ error: err instanceof Error ? err.message : "Invalid request body", }); @@ -314,19 +357,6 @@ export class AnalyticsPlugin extends Plugin { } const format = request.format ?? "JSON"; - const isAsUser = registration.lane === "obo"; - // OBO lane: dispatch via the existing asUser(req) Proxy — same pattern - // used by .obo.sql files in `_handleQueryRoute`. The Proxy threads the - // user's `x-forwarded-access-token` through every Databricks call so - // the warehouse executes the query under the end user's identity. - const executor = isAsUser ? this.asUser(req) : this; - // OBO cache key: hash the user identity so the raw email/principal name - // never reaches the cache layer. SP cache key: literal "sp" — the cache - // is shared across every caller of the SP-lane metric. - const executorKey = deriveMetricExecutorKey({ - lane: registration.lane, - userIdentity: isAsUser ? this.resolveUserId(req) : null, - }); const queryParameters = format === "ARROW" diff --git a/packages/appkit/src/plugins/analytics/metric.ts b/packages/appkit/src/plugins/analytics/metric.ts index 7a3817807..ce2a69a77 100644 --- a/packages/appkit/src/plugins/analytics/metric.ts +++ b/packages/appkit/src/plugins/analytics/metric.ts @@ -471,22 +471,34 @@ export function makeMetricRequestSchema( // are available; values cardinality is enforced per operator; AND/OR // nesting is capped at METRIC_FILTER_MAX_DEPTH. return baseObject.superRefine((value, ctx) => { - // Cross-field rule for timeGrain. We can only enforce "no time-typed - // dimension is grouped" when metadata is available — without it the - // validator falls open (mirrors the dimensions-fall-open behavior at - // line 274-281). The warehouse will reject incompatible grains itself. - if (value.timeGrain != null && Object.keys(grainsByDim).length > 0) { - const dims = value.dimensions ?? []; - const hasTimeDim = dims.some( - (d) => Array.isArray(grainsByDim[d]) && grainsByDim[d].length > 0, - ); - if (!hasTimeDim) { + // Cross-field rule for timeGrain. Tight check whenever the registry has + // ANY dimension metadata: if the metric has dims registered but none are + // time-typed (`grainsByDim` empty), `timeGrain` is meaningless on this + // metric and we reject. The pure-fall-open path now only fires when no + // dimension metadata is available at all — which the route's fail-closed + // gate (`knownMeasures.length === 0` → 503) prevents in practice. + if (value.timeGrain != null && knownDimensions.length > 0) { + const grainsByDimKeys = Object.keys(grainsByDim); + if (grainsByDimKeys.length === 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["timeGrain"], message: - "timeGrain specified but no time-typed dimension is included in 'dimensions'", + "timeGrain specified but the metric has no time-typed dimensions", }); + } else { + const dims = value.dimensions ?? []; + const hasTimeDim = dims.some( + (d) => Array.isArray(grainsByDim[d]) && grainsByDim[d].length > 0, + ); + if (!hasTimeDim) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["timeGrain"], + message: + "timeGrain specified but no time-typed dimension is included in 'dimensions'", + }); + } } } @@ -562,6 +574,19 @@ function validateFilterTree( return; } + // Reject empty `or` groups: SQL-wise an empty disjunction is vacuously + // false, which silently drops the surrounding intent. Empty `and` is OK + // (vacuously true → no constraint contributed). Forcing the caller to + // omit the predicate entirely is the only unambiguous choice. + if (groupKey === "or" && children.length === 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [...path, "or"], + message: "filter 'or' group must contain at least one predicate", + }); + return; + } + children.forEach((child, idx) => { validateFilterTree( child, @@ -653,6 +678,39 @@ function validateFilterTree( }); } } + + // Op⇄value-type compatibility. Catches the malformed-value case at + // validation time (returns 400) instead of letting it surface as a + // synchronous Error from `buildMetricSql` (which would render as 500). + // String operators always require a string value regardless of the + // dimension's declared type. Range operators require a numeric value when + // the dim is numeric — date-typed dims accept ISO date strings, so we + // don't tighten there. + if (STRING_OPERATORS.has(op) && valuesLen > 0) { + const v = predicate.values?.[0]; + if (typeof v !== "string") { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [...path, "values"], + message: `filter operator "${op}" requires a string value (got ${typeof v})`, + }); + } + } + if ( + RANGE_OPERATORS.has(op) && + declaredType && + classifyDimensionType(declaredType) === "numeric" && + valuesLen > 0 + ) { + const v = predicate.values?.[0]; + if (typeof v !== "number") { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [...path, "values"], + message: `filter operator "${op}" on numeric dimension "${predicate.member}" requires a numeric value (got ${typeof v})`, + }); + } + } } /** @@ -719,6 +777,13 @@ function collectAllowedGrains(grainsByDim: Record): string[] { * Returns the parsed body on success; throws {@link ValidationError} with the * canonical 400 shape on failure. Throwing keeps the route handler simple — * the AppKit error pipeline handles the response shape. + * + * The thrown error's public `message` carries only the offending field paths + * (`measures.0`, `filter.and.0.member`, etc.) — never the registry's allowed + * values or the metric's measure/dimension names. The full Zod issue list, + * including allowlists embedded in per-issue messages, is preserved on + * `context.issues` for server-side telemetry. This prevents an unauthenticated + * caller from enumerating the registered schema by sending malformed bodies. */ export function validateMetricRequest( registration: MetricRegistration, @@ -727,15 +792,20 @@ export function validateMetricRequest( const schema = makeMetricRequestSchema(registration); const result = schema.safeParse(body); if (!result.success) { - const detail = result.error.issues - .map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`) - .join("; "); - throw new ValidationError(`Invalid metric request body: ${detail}`, { - context: { - metric: registration.key, - issues: result.error.issues, + const fieldPaths = result.error.issues + .map((i) => i.path.join(".") || "(root)") + .join(", "); + throw new ValidationError( + fieldPaths.length > 0 + ? `Invalid metric request body (fields: ${fieldPaths})` + : "Invalid metric request body", + { + context: { + metric: registration.key, + issues: result.error.issues, + }, }, - }); + ); } return result.data; } @@ -978,8 +1048,14 @@ function renderFilter( )[groupKey]; if (!Array.isArray(children) || children.length === 0) { - // Empty group → no constraint; an empty AND is vacuously true and an - // empty OR is the validator's "do not contribute" shape. + // Empty AND → vacuously true, render as no constraint (null). + // Empty OR → vacuously false. The validator rejects this case before + // reaching the SQL builder, but if it slips through, render `1 = 0` + // rather than dropping the predicate silently — defense in depth so a + // future validator bypass cannot turn `or: []` into "match everything". + if (groupKey === "or") { + return "1 = 0"; + } return null; } @@ -1358,10 +1434,12 @@ function canonicalizeFilter(node: MetricFilter): string { return `${groupKey}(${childFingerprints.join(",")})`; } - // Leaf predicate. + // Leaf predicate. Use JSON.stringify (not String) for the value segment so + // strings carrying the `|` separator cannot collide with split arrays — + // e.g. `["a", "b"]` and `["a|string:b"]` are now distinct fingerprints. const p = node as MetricPredicate; const valuesPart = p.values - ? p.values.map((v) => `${typeof v}:${String(v)}`).join("|") + ? p.values.map((v) => `${typeof v}:${JSON.stringify(v)}`).join("|") : ""; return `p(${p.member}/${p.operator}/${valuesPart})`; } diff --git a/packages/appkit/src/plugins/analytics/tests/metric.test.ts b/packages/appkit/src/plugins/analytics/tests/metric.test.ts index a0c7fc68a..cf8a1481c 100644 --- a/packages/appkit/src/plugins/analytics/tests/metric.test.ts +++ b/packages/appkit/src/plugins/analytics/tests/metric.test.ts @@ -117,7 +117,7 @@ describe("metric — pure helpers", () => { validateMetricRequest(REVENUE_REGISTRATION, { measures: [], }), - ).toThrowError(/measures must contain at least one entry/); + ).toThrowError(/fields:.*measures/); }); test("rejects a non-positive limit", () => { @@ -126,7 +126,7 @@ describe("metric — pure helpers", () => { measures: ["arr"], limit: -1, }), - ).toThrowError(/limit must be positive/); + ).toThrowError(/fields:.*limit/); }); test("rejects unknown top-level fields (strict)", () => { @@ -223,7 +223,7 @@ describe("metric — pure helpers", () => { dimensions: ["created_at"], timeGrain: "year", }), - ).toThrowError(/timeGrain must be one of/); + ).toThrowError(/fields:.*timeGrain/); }); test("rejects timeGrain when no time-typed dim is in dimensions", () => { @@ -233,7 +233,7 @@ describe("metric — pure helpers", () => { dimensions: ["region"], timeGrain: "month", }), - ).toThrowError(/no time-typed dimension/); + ).toThrowError(/fields:.*timeGrain/); }); test("rejects timeGrain when dimensions is omitted entirely", () => { @@ -242,7 +242,26 @@ describe("metric — pure helpers", () => { measures: ["arr"], timeGrain: "month", }), - ).toThrowError(/no time-typed dimension/); + ).toThrowError(/fields:.*timeGrain/); + }); + + test("rejects timeGrain when metric has registered dims but none are time-typed", () => { + // Tighter validation: when the registry knows the metric's dims but + // none of them carry a time-grain set, `timeGrain` is meaningless on + // this metric. Earlier this case fell open (validator skipped on empty + // grainsByDim). + const noTimeRegistration: MetricRegistration = { + ...REVENUE_REGISTRATION, + knownDimensions: ["region", "segment"], + knownTimeGrainsByDim: {}, + }; + expect(() => + validateMetricRequest(noTimeRegistration, { + measures: ["arr"], + dimensions: ["region"], + timeGrain: "month", + }), + ).toThrowError(/fields:.*timeGrain/); }); test("rejects timeGrain when none of the requested dims are time-typed (metadata available)", () => { @@ -530,6 +549,32 @@ describe("metric — pure helpers", () => { expect(a).not.toEqual(b); }); + test("filter values containing the `|` separator do not collide across distinct shapes", () => { + // Regression: an earlier `String(v)` join used `|` as a separator, + // making `["a", "b"]` collapse with `["a|string:b"]`. The fingerprint + // must distinguish them so the SP cache cannot serve a different + // user's results to a caller with a colliding-shaped filter. + const a = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + filter: { member: "region", operator: "in", values: ["a", "b"] }, + format: "JSON", + executorKey: "sp", + }); + const b = composeMetricCacheKey({ + metricKey: "revenue", + measures: ["arr"], + filter: { + member: "region", + operator: "in", + values: ["a|string:b"], + }, + format: "JSON", + executorKey: "sp", + }); + expect(a).not.toEqual(b); + }); + // ── Phase 2: dimensions + timeGrain ───────────────────────────────── test("normalizes dimension order for cache hits across equivalent calls", () => { const a = composeMetricCacheKey({ @@ -809,6 +854,38 @@ describe("AnalyticsPlugin — metric route handler", () => { }); }); + test("returns 503 when the registered metric has no build-time metadata (fail-closed)", async () => { + // Defense-in-depth: when `metrics.metadata.json` is missing or didn't + // populate measures for this metric, the validator falls open and the + // SQL constructor would let arbitrary identifiers through to the + // warehouse — a schema-enumeration vector. Refuse the request. + const plugin = new AnalyticsPlugin(config); + plugin._setMetricRegistryForTesting({ + revenue: { + ...REVENUE_REGISTRATION, + knownMeasures: [], + knownDimensions: [], + }, + }); + const { router, getHandler } = createMockRouter(); + plugin.injectRoutes(router); + + const handler = getHandler("POST", "/metric/:key"); + const mockReq = createMockRequest({ + params: { key: "revenue" }, + body: { measures: ["arr"] }, + }); + const mockRes = createMockResponse(); + + await handler(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(503); + const errorPayload = (mockRes.json as any).mock.calls[0][0]; + expect(errorPayload.code).toBe("METRIC_REGISTRY_NOT_READY"); + // Generic message — does not name the metric or the build-time tooling. + expect(errorPayload.error).toBe("Metric registry not initialized"); + }); + test("returns 400 with the canonical error shape on validator failure", async () => { const plugin = new AnalyticsPlugin(config); plugin._setMetricRegistryForTesting({ @@ -890,7 +967,7 @@ describe("AnalyticsPlugin — metric route handler", () => { expect(mockRes.setHeader).toHaveBeenCalledWith( "Content-Type", - "text/event-stream", + expect.stringContaining("text/event-stream"), ); expect(mockRes.write).toHaveBeenCalledWith("event: result\n"); expect(mockRes.write).toHaveBeenCalledWith( @@ -1025,8 +1102,11 @@ describe("AnalyticsPlugin — metric route handler", () => { expect(mockRes.status).toHaveBeenCalledWith(400); const errorPayload = (mockRes.json as any).mock.calls[0][0]; - expect(errorPayload.error).toMatch(/no time-typed dimension/); + expect(errorPayload.error).toMatch(/fields:.*timeGrain/); expect(errorPayload.code).toBe("VALIDATION_ERROR"); + // Defense-in-depth: the public 400 must not enumerate the registered + // schema (allowed grain enum, dim allowlist, etc.) — only the field path. + expect(errorPayload.error).not.toMatch(/must be one of|no time-typed/); }); test("returns 400 when an unknown dimension is requested", async () => { @@ -1342,7 +1422,7 @@ describe("metric — filter translator", () => { measures: ["arr"], filter: node, }), - ).toThrowError(/maximum depth/); + ).toThrowError(/fields:.*filter/); }); test("accepts exactly 8 levels of AND nesting (validator)", () => { @@ -1430,7 +1510,7 @@ describe("metric — filter translator", () => { values: ["x"], }, }), - ).toThrowError(/not a declared dimension/); + ).toThrowError(/fields:.*filter\.member/); }); test("rejects an unknown operator", () => { @@ -1443,7 +1523,7 @@ describe("metric — filter translator", () => { values: ["E"], }, }), - ).toThrowError(/not one of/); + ).toThrowError(/fields:.*filter\.operator/); }); test("rejects gt on a string-typed dimension (op⇄type)", () => { @@ -1456,7 +1536,7 @@ describe("metric — filter translator", () => { values: ["EMEA"], }, }), - ).toThrowError(/incompatible/); + ).toThrowError(/fields:.*filter\.operator/); }); test("rejects contains on a numeric-typed dimension (op⇄type)", () => { @@ -1469,7 +1549,7 @@ describe("metric — filter translator", () => { values: ["1000"], }, }), - ).toThrowError(/incompatible/); + ).toThrowError(/fields:.*filter\.operator/); }); test("rejects contains on a date-typed dimension (op⇄type)", () => { @@ -1482,7 +1562,7 @@ describe("metric — filter translator", () => { values: ["2026"], }, }), - ).toThrowError(/incompatible/); + ).toThrowError(/fields:.*filter\.operator/); }); test("accepts gt on a date-typed dimension", () => { @@ -1508,7 +1588,7 @@ describe("metric — filter translator", () => { values: [], }, }), - ).toThrowError(/exactly one value/); + ).toThrowError(/fields:.*filter\.values/); }); test("rejects equals with multiple values (cardinality)", () => { @@ -1521,7 +1601,7 @@ describe("metric — filter translator", () => { values: ["A", "B"], }, }), - ).toThrowError(/exactly one value/); + ).toThrowError(/fields:.*filter\.values/); }); test("rejects in with empty values (cardinality)", () => { @@ -1534,7 +1614,7 @@ describe("metric — filter translator", () => { values: [], }, }), - ).toThrowError(/at least one value/); + ).toThrowError(/fields:.*filter\.values/); }); test("rejects set with values (cardinality — must be absent)", () => { @@ -1547,7 +1627,7 @@ describe("metric — filter translator", () => { values: ["EMEA"], }, }), - ).toThrowError(/must not carry values/); + ).toThrowError(/fields:.*filter\.values/); }); test("accepts set with no values", () => { @@ -1592,6 +1672,54 @@ describe("metric — filter translator", () => { ).not.toThrow(); }); + test("rejects empty `or` group (empty disjunction is vacuously false)", () => { + // Empty AND is vacuously true (no constraint). Empty OR would be + // vacuously false — silently dropping the predicate. Force the caller + // to omit the predicate entirely so intent stays explicit. + expect(() => + validateMetricRequest(REVENUE_PHASE3_REGISTRATION, { + measures: ["arr"], + filter: { or: [] }, + }), + ).toThrowError(/fields:.*filter\.or/); + }); + + test("accepts empty `and` group (no constraint contributed)", () => { + // Empty AND is the validator's "do not contribute" shape — accepted. + expect(() => + validateMetricRequest(REVENUE_PHASE3_REGISTRATION, { + measures: ["arr"], + filter: { and: [] }, + }), + ).not.toThrow(); + }); + + test("rejects `contains` with a non-string value", () => { + expect(() => + validateMetricRequest(REVENUE_PHASE3_REGISTRATION, { + measures: ["arr"], + filter: { + member: "region", + operator: "contains", + values: [42 as unknown as string], + }, + }), + ).toThrowError(/fields:.*filter\.values/); + }); + + test("rejects range op with a non-numeric value on a numeric dim", () => { + expect(() => + validateMetricRequest(REVENUE_PHASE3_REGISTRATION, { + measures: ["arr"], + filter: { + member: "deal_size", + operator: "gt", + values: ["large" as unknown as number], + }, + }), + ).toThrowError(/fields:.*filter\.values/); + }); + test("rejects member at depth — nested filter with unknown member", () => { expect(() => validateMetricRequest(REVENUE_PHASE3_REGISTRATION, { @@ -1603,7 +1731,7 @@ describe("metric — filter translator", () => { ], }, }), - ).toThrowError(/not a declared dimension/); + ).toThrowError(/fields:.*filter\.and\.1\.member/); }); }); @@ -1793,7 +1921,12 @@ describe("metric — filter translator", () => { expect(mockRes.status).toHaveBeenCalledWith(400); const errorPayload = (mockRes.json as any).mock.calls[0][0]; expect(errorPayload.code).toBe("VALIDATION_ERROR"); - expect(errorPayload.error).toMatch(/not a declared dimension/); + expect(errorPayload.error).toMatch(/fields:.*filter\.member/); + // Defense-in-depth: the public 400 must not name the registry's + // allowed dimensions. The full Zod issues stay in telemetry context. + expect(errorPayload.error).not.toMatch( + /not a declared dimension|allowed:|must be one of/, + ); }); }); }); diff --git a/packages/appkit/src/type-generator/index.ts b/packages/appkit/src/type-generator/index.ts index 6491e7bb8..384924dc5 100644 --- a/packages/appkit/src/type-generator/index.ts +++ b/packages/appkit/src/type-generator/index.ts @@ -7,7 +7,11 @@ import { type DescribeFetcher, generateMetricsMetadataJson, generateMetricTypeDeclarations, + type MetricColumnMetadata, + type MetricLane, type MetricSchema, + type MetricSyncFailure, + type MetricSyncResult, readMetricConfig, resolveMetricConfig, syncMetrics, @@ -131,11 +135,26 @@ export async function generateFromEntryPoint(options: { const resolution = resolveMetricConfig(metricConfig); const fetcher = metricFetcher ?? createWorkspaceDescribeFetcher(warehouseId); - const metricSchemas: MetricSchema[] = await syncMetrics( + const { schemas: metricSchemas, failures } = await syncMetrics( resolution, fetcher, ); + // Surface DESCRIBE failures loudly so a misconfigured metric.json or a + // workspace-side typo doesn't silently ship an empty bundle entry. The + // route's runtime fail-closed gate would 503 these in production — + // catching the issue at type-gen time is the cheaper signal. + if (failures.length > 0) { + for (const f of failures) { + logger.warn( + "metric sync failed for %s (%s): %s", + f.key, + f.source, + f.reason, + ); + } + } + const metricFile = metricOutFile ?? path.join(path.dirname(outFile), METRIC_TYPES_FILE); const metricDeclarations = generateMetricTypeDeclarations(metricSchemas); @@ -153,8 +172,9 @@ export async function generateFromEntryPoint(options: { await fs.writeFile(metadataFile, metadataJson, "utf-8"); logger.debug( - "Wrote MetricRegistry augmentation + metadata bundle for %d metric(s)", + "Wrote MetricRegistry augmentation + metadata bundle for %d metric(s)%s", metricSchemas.length, + failures.length > 0 ? ` (${failures.length} failure(s))` : "", ); } } @@ -171,6 +191,17 @@ export async function generateFromEntryPoint(options: { // mirroring how generateFromEntryPoint (also defined here) is preserved via the analytics vite plugin. export const generateServingTypes = generateServingTypesImpl; +// Re-export the metric-registry types so consumers (CLI, the type-generator +// .d.ts shim in `packages/shared`) can pick them up from this entry point — +// the .d.ts shim documents these as part of the package's public surface. +export type { + MetricColumnMetadata, + MetricLane, + MetricSchema, + MetricSyncFailure, + MetricSyncResult, +}; + /** Directory name for generated AppKit type declaration files. */ export const TYPES_DIR = "appkit-types"; /** Default filename for analytics query type declarations. */ diff --git a/packages/appkit/src/type-generator/metric-registry.ts b/packages/appkit/src/type-generator/metric-registry.ts index fa9a2c3d1..d60cee1fb 100644 --- a/packages/appkit/src/type-generator/metric-registry.ts +++ b/packages/appkit/src/type-generator/metric-registry.ts @@ -815,10 +815,14 @@ interface MetricColumnSemanticMetadata { * Splits cleanly into measures + dimensions so the consuming hook can return * the exact subset for the queried metric without scanning the rest of the * registry. + * + * Server-side concerns — UC FQN (`source`) and execution lane (`lane`) — are + * deliberately NOT part of this artifact. They live in `metric.json` and are + * consumed by the server only. The bundle ships to the client in + * `metrics.metadata.json` and must contain frontend-safe metadata only + * (display names, format specs, descriptions, time-grain hints). */ interface MetricSemanticMetadataEntry { - source: string; - lane: MetricLane; measures: Record; dimensions: Record; } @@ -865,8 +869,6 @@ export function buildMetricsMetadataBundle( } bundle[schema.key] = { - source: schema.source, - lane: schema.lane, measures, dimensions, }; @@ -938,28 +940,60 @@ export function createWorkspaceDescribeFetcher( }; } +/** + * One per-entry sync failure recorded by {@link syncMetrics}. Failures are + * surfaced to the caller (CLI / Vite plugin) so they can decide whether to + * exit non-zero. Without this, a silently-empty bundle would ship to + * production and the route's runtime fail-closed gate would 503 every + * affected metric. + */ +export interface MetricSyncFailure { + /** Stable metric key — matches the key in metric.json. */ + key: string; + /** Three-part FQN that failed to resolve. */ + source: string; + /** Single human-readable reason (DESCRIBE failed, parse failed, zero columns). */ + reason: string; +} + +/** + * Result shape from {@link syncMetrics}: the schemas (one per entry, possibly + * empty if the entry failed) plus a list of per-entry failures so the caller + * can emit a non-zero exit / build error when something didn't resolve. + */ +export interface MetricSyncResult { + schemas: MetricSchema[]; + failures: MetricSyncFailure[]; +} + /** * Run schema synchronization for every entry in `metric.json`. * - * `fetcher` is injected so the same code path serves Vite, the (Phase 6) CLI, - * and unit tests with a mock that returns a representative DESCRIBE response. + * `fetcher` is injected so the same code path serves Vite, the CLI, and unit + * tests with a mock that returns a representative DESCRIBE response. + * + * Returns `{ schemas, failures }`. The schemas array always carries one + * entry per registered metric (even on failure — the entry has empty + * measures/dimensions). The failures array is populated for any entry that + * (a) the DESCRIBE call rejected, (b) the response could not be parsed, or + * (c) extraction yielded zero columns. Callers (the CLI, the Vite plugin) + * inspect `failures` to decide whether to exit non-zero. */ export async function syncMetrics( resolution: MetricConfigResolution, fetcher: DescribeFetcher, -): Promise { +): Promise { const schemas: MetricSchema[] = []; + const failures: MetricSyncFailure[] = []; for (const entry of resolution.entries) { let response: DatabricksStatementExecutionResponse; try { response = await fetcher(entry.source); } catch (err) { - logger.warn( - "DESCRIBE TABLE EXTENDED failed for %s: %s", - entry.source, - (err as Error).message, - ); + const reason = `DESCRIBE TABLE EXTENDED failed: ${(err as Error).message}`; + logger.warn("%s for %s", reason, entry.source); + failures.push({ key: entry.key, source: entry.source, reason }); schemas.push({ key: entry.key, source: entry.source, @@ -971,31 +1005,34 @@ export async function syncMetrics( } let columns: MetricColumnMetadata[] = []; + let parseError: string | null = null; try { const parsed = parseDescribeTableExtendedJson(response); columns = extractMetricColumns(parsed); } catch (err) { - logger.warn( - "Failed to extract columns from DESCRIBE response for %s: %s", - entry.source, - (err as Error).message, - ); + parseError = `Failed to extract columns from DESCRIBE response: ${(err as Error).message}`; + logger.warn("%s for %s", parseError, entry.source); } const measures = columns.filter((c) => c.isMeasure); const dimensions = columns.filter((c) => !c.isMeasure); - // Warn when extraction succeeded but yielded no columns. The most common - // cause is a DESCRIBE response shape that `extractMetricColumns` doesn't - // recognize (e.g., joined metric views may put columns under a wrapper - // that the v0.1 reader doesn't traverse). Without this signal, the bundle - // ships with empty measures/dimensions and downstream consumers blow up - // on `metadata.measures..format` accesses. - if (columns.length === 0) { - logger.warn( - "DESCRIBE response for %s yielded zero columns — the bundle entry will have empty measures/dimensions. Check that the response shape has a top-level `columns` array (or `schema.fields`).", - entry.source, - ); + if (parseError) { + failures.push({ + key: entry.key, + source: entry.source, + reason: parseError, + }); + } else if (columns.length === 0) { + // Extraction succeeded but yielded no columns. The most common cause + // is a DESCRIBE response shape that `extractMetricColumns` doesn't + // recognize. Treat as a failure so CI catches it instead of letting an + // empty bundle entry ship — the route's fail-closed gate would then + // 503 every request to this metric in production. + const reason = + "DESCRIBE response yielded zero columns — check the response shape (top-level `columns` array or `schema.fields`)."; + logger.warn("%s for %s", reason, entry.source); + failures.push({ key: entry.key, source: entry.source, reason }); } schemas.push({ @@ -1007,5 +1044,5 @@ export async function syncMetrics( }); } - return schemas; + return { schemas, failures }; } diff --git a/packages/appkit/src/type-generator/tests/__snapshots__/metric-registry.test.ts.snap b/packages/appkit/src/type-generator/tests/__snapshots__/metric-registry.test.ts.snap index c60fc988f..f8fbaef9c 100644 --- a/packages/appkit/src/type-generator/tests/__snapshots__/metric-registry.test.ts.snap +++ b/packages/appkit/src/type-generator/tests/__snapshots__/metric-registry.test.ts.snap @@ -115,8 +115,6 @@ declare module "@databricks/appkit-ui/react" { exports[`generateMetricsMetadataJson — snapshot > serializes a representative metric view with display_name + format + time_grain 1`] = ` "{ "customer_metrics": { - "source": "appkit_demo.public.customer_metrics", - "lane": "obo", "measures": { "churn_rate": { "type": "DOUBLE", @@ -132,8 +130,6 @@ exports[`generateMetricsMetadataJson — snapshot > serializes a representative } }, "revenue": { - "source": "appkit_demo.public.revenue_metrics", - "lane": "sp", "measures": { "arr": { "type": "DECIMAL(38,2)", diff --git a/packages/appkit/src/type-generator/tests/metric-registry.test.ts b/packages/appkit/src/type-generator/tests/metric-registry.test.ts index 419abde4d..ec4d32608 100644 --- a/packages/appkit/src/type-generator/tests/metric-registry.test.ts +++ b/packages/appkit/src/type-generator/tests/metric-registry.test.ts @@ -329,7 +329,7 @@ describe("syncMetrics", () => { ], }); - const schemas = await syncMetrics(resolution, fetcher); + const { schemas } = await syncMetrics(resolution, fetcher); expect(schemas).toHaveLength(1); const [schema] = schemas; expect(schema.key).toBe("revenue"); @@ -346,7 +346,7 @@ describe("syncMetrics", () => { throw new Error("warehouse unreachable"); }; - const schemas = await syncMetrics(resolution, fetcher); + const { schemas } = await syncMetrics(resolution, fetcher); expect(schemas[0].measures).toEqual([]); expect(schemas[0].dimensions).toEqual([]); }); @@ -378,7 +378,7 @@ describe("generateMetricTypeDeclarations — snapshot", () => { ], }); - const schemas = await syncMetrics(resolution, fetcher); + const { schemas } = await syncMetrics(resolution, fetcher); const output = generateMetricTypeDeclarations(schemas); expect(output).toMatchSnapshot(); }); @@ -411,7 +411,7 @@ describe("generateMetricTypeDeclarations — snapshot", () => { ], }); - const schemas = await syncMetrics(resolution, fetcher); + const { schemas } = await syncMetrics(resolution, fetcher); const output = generateMetricTypeDeclarations(schemas); expect(output).toMatchSnapshot(); @@ -736,12 +736,10 @@ describe("buildMetricsMetadataBundle", () => { ], }); - const schemas = await syncMetrics(resolution, fetcher); + const { schemas } = await syncMetrics(resolution, fetcher); const bundle = buildMetricsMetadataBundle(schemas); expect(bundle.revenue).toMatchObject({ - source: "appkit_demo.public.revenue_metrics", - lane: "sp", measures: { arr: { type: "DECIMAL(38,2)", @@ -768,6 +766,10 @@ describe("buildMetricsMetadataBundle", () => { }, }, }); + // Defense-in-depth: the client-shipped bundle must not carry server-side + // concerns (UC FQN, execution lane). They live in metric.json server-side. + expect(bundle.revenue).not.toHaveProperty("source"); + expect(bundle.revenue).not.toHaveProperty("lane"); }); test("preserves stable alphabetical key order across metrics", async () => { @@ -783,7 +785,7 @@ describe("buildMetricsMetadataBundle", () => { columns: [{ name: "v", type: "DECIMAL", is_measure: true }], }); - const schemas = await syncMetrics(resolution, fetcher); + const { schemas } = await syncMetrics(resolution, fetcher); const bundle = buildMetricsMetadataBundle(schemas); expect(Object.keys(bundle)).toEqual(["a_metric", "z_metric"]); }); @@ -798,7 +800,7 @@ describe("buildMetricsMetadataBundle", () => { columns: [{ name: "arr", type: "DECIMAL", is_measure: true }], }); - const schemas = await syncMetrics(resolution, fetcher); + const { schemas } = await syncMetrics(resolution, fetcher); const bundle = buildMetricsMetadataBundle(schemas); const arr = bundle.revenue.measures.arr; expect(arr.type).toBe("DECIMAL"); @@ -825,7 +827,7 @@ describe("buildMetricsMetadataBundle", () => { ], }); - const schemas = await syncMetrics(resolution, fetcher); + const { schemas } = await syncMetrics(resolution, fetcher); const bundle = buildMetricsMetadataBundle(schemas); expect(bundle.revenue.measures.last_event_at.time_grain).toBeUndefined(); expect(bundle.revenue.dimensions.ts.time_grain).toEqual([ @@ -905,7 +907,7 @@ describe("generateMetricsMetadataJson — snapshot", () => { ], }); - const schemas = await syncMetrics(resolution, fetcher); + const { schemas } = await syncMetrics(resolution, fetcher); const json = generateMetricsMetadataJson(schemas); expect(json).toMatchSnapshot(); @@ -927,7 +929,14 @@ describe("generateMetricsMetadataJson — snapshot", () => { "week", "year", ]); - expect(parsed.customer_metrics.lane).toBe("obo"); + // The client-shipped artifact must not carry server-side concerns: + // UC FQN (`source`) and execution lane (`lane`) live in metric.json + // and are consumed only on the server. Asserting their absence catches + // accidental re-introduction in code review or refactors. + expect(parsed.revenue).not.toHaveProperty("source"); + expect(parsed.revenue).not.toHaveProperty("lane"); + expect(parsed.customer_metrics).not.toHaveProperty("source"); + expect(parsed.customer_metrics).not.toHaveProperty("lane"); }); test("emits `{}` when no metrics are registered", () => { @@ -951,7 +960,7 @@ describe("syncMetrics — time-typed dimension propagation", () => { ], }); - const schemas = await syncMetrics(resolution, fetcher); + const { schemas } = await syncMetrics(resolution, fetcher); expect(schemas[0].dimensions).toHaveLength(2); const tsDim = schemas[0].dimensions.find((d) => d.name === "ts"); expect(tsDim?.timeGrains).toEqual([ diff --git a/packages/shared/src/cli/commands/metric/sync/sync.test.ts b/packages/shared/src/cli/commands/metric/sync/sync.test.ts index 500ce42c5..4a6602ced 100644 --- a/packages/shared/src/cli/commands/metric/sync/sync.test.ts +++ b/packages/shared/src/cli/commands/metric/sync/sync.test.ts @@ -58,7 +58,9 @@ function makeDeps( dimensions: [], }); } - return schemas; + // The mock returns no failures by default — tests that need to + // exercise the failures-surfacing path override this seam. + return { schemas, failures: [] }; }), resolveMetricConfig: vi.fn((config) => { const cfg = config as { @@ -507,4 +509,43 @@ describe("runMetricSync — failure modes", () => { } } }); + + it("surfaces per-entry sync failures (parse / zero-column) as a typed error", async () => { + // Simulates the case where DESCRIBE returned successfully but extraction + // produced an empty bundle — without surfacing this, an empty bundle + // would ship and the runtime fail-closed gate would 503 every request. + const io = captureIO(); + const deps = makeDeps({ + syncMetrics: vi.fn( + async (resolution: { + entries: Array<{ key: string; source: string; lane: "sp" | "obo" }>; + }) => ({ + schemas: resolution.entries.map((e) => ({ + key: e.key, + source: e.source, + lane: e.lane, + measures: [], + dimensions: [], + })), + failures: [ + { + key: "revenue", + source: "appkit_demo.public.revenue_metrics", + reason: "DESCRIBE response yielded zero columns", + }, + ], + }), + ), + }); + + await expect( + runMetricSync( + { + warehouseId: "wh-x", + rootDir: tmp, + }, + { ...io, deps, interactive: false }, + ), + ).rejects.toThrowError(/zero columns/); + }); }); diff --git a/packages/shared/src/cli/commands/metric/sync/sync.ts b/packages/shared/src/cli/commands/metric/sync/sync.ts index 2b2cde7b3..2b20d9358 100644 --- a/packages/shared/src/cli/commands/metric/sync/sync.ts +++ b/packages/shared/src/cli/commands/metric/sync/sync.ts @@ -234,13 +234,27 @@ export interface MetricSyncSchema { * loading the full ESM module graph (which would require `@databricks/appkit` * to be built before tests run). */ +/** + * Per-entry sync failure surfaced by `syncMetrics()`. Mirrors the shape in + * `@databricks/appkit/type-generator`'s `MetricSyncFailure`. Defined + * structurally here so the CLI doesn't load the appkit package at type-time. + */ +export interface MetricSyncFailureLite { + key: string; + source: string; + reason: string; +} + export interface MetricSyncDependencies { syncMetrics: ( resolution: { entries: Array<{ key: string; source: string; lane: "sp" | "obo" }>; }, fetcher: (fqn: string) => Promise, - ) => Promise; + ) => Promise<{ + schemas: MetricSyncSchema[]; + failures: MetricSyncFailureLite[]; + }>; resolveMetricConfig: (config: unknown) => { entries: Array<{ key: string; source: string; lane: "sp" | "obo" }>; }; @@ -515,7 +529,10 @@ export async function runMetricSync( ); } - const schemas = await deps.syncMetrics(resolution, wrappedFetcher); + const { schemas, failures } = await deps.syncMetrics( + resolution, + wrappedFetcher, + ); // If any entry's fetch failed, surface the first failure as a typed error. // We deliberately defer this until after `syncMetrics()` returns so the @@ -524,6 +541,23 @@ export async function runMetricSync( throw firstFailure; } + // Even when no fetch threw (typed network errors above), `syncMetrics` may + // record per-entry failures: response parse errors, zero-column extraction. + // These represent silent corruption — the bundle would ship empty and the + // route's runtime fail-closed gate would 503 every request. Surface them + // as a typed CLI error so CI catches it instead of letting an empty bundle + // ship. + if (failures.length > 0) { + const summary = failures + .map((f) => ` ${f.key} (${f.source}): ${f.reason}`) + .join("\n"); + throw new MetricSyncError( + "unknown", + `metric sync produced ${failures.length} failure(s):\n${summary}`, + failures[0]?.source, + ); + } + // Step 4: Emit artifacts. `outputDir` is created (recursively) on first use. fs.mkdirSync(inputs.outputDir, { recursive: true }); diff --git a/packages/shared/src/cli/commands/type-generator.d.ts b/packages/shared/src/cli/commands/type-generator.d.ts index 3608b970f..b67e5d525 100644 --- a/packages/shared/src/cli/commands/type-generator.d.ts +++ b/packages/shared/src/cli/commands/type-generator.d.ts @@ -54,6 +54,19 @@ declare module "@databricks/appkit/type-generator" { export type DescribeFetcher = (fqn: string) => Promise; + /** Per-entry sync failure surfaced by `syncMetrics()`. */ + export interface MetricSyncFailure { + key: string; + source: string; + reason: string; + } + + /** Result of `syncMetrics()`: schemas + per-entry failures. */ + export interface MetricSyncResult { + schemas: MetricSchema[]; + failures: MetricSyncFailure[]; + } + export function readMetricConfig( queryFolder: string, ): Promise; @@ -63,7 +76,7 @@ declare module "@databricks/appkit/type-generator" { export function syncMetrics( resolution: MetricConfigResolution, fetcher: DescribeFetcher, - ): Promise; + ): Promise; export function createWorkspaceDescribeFetcher( warehouseId: string, ): DescribeFetcher; From ed61b33be278ab0f1f1c170eb5578587527dffb0 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Mon, 4 May 2026 14:21:54 +0200 Subject: [PATCH 19/34] fix(playground): regenerate client lockfile against the public registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous lockfile had 268 resolved URLs pointing at the internal npm-proxy.dev.databricks.com proxy (plotly.js transitive deps — mapbox, choojs, etc.). Those got baked in when plotly was added on a developer machine whose ~/.npmrc routes through the internal proxy. CI runners cannot reach npm-proxy.dev.databricks.com — when pnpm's lifecycle hook ran `cd client && npm install`, npm tried to fetch from the proxy URLs in the lockfile, hung for 8 minutes, and crashed with `Exit handler never called!`. Both prior CI runs on this PR failed at this step. Regenerating with --package-lock-only (post-rewriting the host portion of the @esbuild/* platform-optional entries that retained explicit `resolved` URLs) produces a lockfile with zero proxy URLs and 59 entries on registry.npmjs.org. The other 695 packages defer to whatever registry the install user has configured at install time. Co-authored-by: Isaac --- apps/dev-playground/client/package-lock.json | 1485 +----------------- 1 file changed, 60 insertions(+), 1425 deletions(-) diff --git a/apps/dev-playground/client/package-lock.json b/apps/dev-playground/client/package-lock.json index 80585ebd1..0f2eae0eb 100644 --- a/apps/dev-playground/client/package-lock.json +++ b/apps/dev-playground/client/package-lock.json @@ -50,35 +50,8 @@ "vite": "npm:rolldown-vite@7.1.14" } }, - "../../../packages/appkit-ui": { - "name": "@databricks/appkit-ui", - "version": "1.0.0", - "extraneous": true, - "dependencies": { - "clsx": "^2.1.1", - "shared": "workspace:*", - "tailwind-merge": "^3.4.0" - }, - "devDependencies": { - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "recharts": "^3.4.1" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0", - "recharts": "^2.0.0 || ^3.0.0" - } - }, - "../../../packages/appkit-ui/dist": { - "extraneous": true - }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", "dev": true, "license": "MIT", "engines": { @@ -90,8 +63,6 @@ }, "node_modules/@babel/code-frame": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", @@ -104,8 +75,6 @@ }, "node_modules/@babel/compat-data": { "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", - "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -113,8 +82,6 @@ }, "node_modules/@babel/core": { "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", - "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -143,8 +110,6 @@ }, "node_modules/@babel/generator": { "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", @@ -159,8 +124,6 @@ }, "node_modules/@babel/helper-annotate-as-pure": { "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "license": "MIT", "dependencies": { "@babel/types": "^7.27.3" @@ -171,8 +134,6 @@ }, "node_modules/@babel/helper-compilation-targets": { "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "license": "MIT", "dependencies": { "@babel/compat-data": "^7.27.2", @@ -187,8 +148,6 @@ }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", - "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -208,8 +167,6 @@ }, "node_modules/@babel/helper-globals": { "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -217,8 +174,6 @@ }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.28.5", @@ -230,8 +185,6 @@ }, "node_modules/@babel/helper-module-imports": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.28.6", @@ -243,8 +196,6 @@ }, "node_modules/@babel/helper-module-transforms": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.28.6", @@ -260,8 +211,6 @@ }, "node_modules/@babel/helper-optimise-call-expression": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", "license": "MIT", "dependencies": { "@babel/types": "^7.27.1" @@ -272,8 +221,6 @@ }, "node_modules/@babel/helper-plugin-utils": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -281,8 +228,6 @@ }, "node_modules/@babel/helper-replace-supers": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", - "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", "license": "MIT", "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", @@ -298,8 +243,6 @@ }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -311,8 +254,6 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -320,8 +261,6 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -329,8 +268,6 @@ }, "node_modules/@babel/helper-validator-option": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -338,8 +275,6 @@ }, "node_modules/@babel/helpers": { "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", @@ -351,8 +286,6 @@ }, "node_modules/@babel/parser": { "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -366,8 +299,6 @@ }, "node_modules/@babel/plugin-syntax-jsx": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", - "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -381,8 +312,6 @@ }, "node_modules/@babel/plugin-syntax-typescript": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", - "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -396,8 +325,6 @@ }, "node_modules/@babel/plugin-transform-modules-commonjs": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", - "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.28.6", @@ -412,8 +339,6 @@ }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", "dev": true, "license": "MIT", "dependencies": { @@ -428,8 +353,6 @@ }, "node_modules/@babel/plugin-transform-react-jsx-source": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", "dev": true, "license": "MIT", "dependencies": { @@ -444,8 +367,6 @@ }, "node_modules/@babel/plugin-transform-typescript": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", - "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -463,8 +384,6 @@ }, "node_modules/@babel/preset-typescript": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", - "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -482,8 +401,6 @@ }, "node_modules/@babel/template": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.28.6", @@ -496,8 +413,6 @@ }, "node_modules/@babel/traverse": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.0", @@ -514,8 +429,6 @@ }, "node_modules/@babel/types": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -527,8 +440,6 @@ }, "node_modules/@choojs/findup": { "version": "0.2.1", - "resolved": "https://npm-proxy.dev.databricks.com/@choojs/findup/-/findup-0.2.1.tgz", - "integrity": "sha512-YstAqNb0MCN8PjdLCDfRsBcGVRN41f3vgLvaI0IrIcBp4AqILRSS0DeWNGkicC+f/zRIPJLc+9RURVSepwvfBw==", "license": "MIT", "dependencies": { "commander": "^2.15.1" @@ -539,8 +450,6 @@ }, "node_modules/@emnapi/core": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", - "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", "license": "MIT", "optional": true, "dependencies": { @@ -550,8 +459,6 @@ }, "node_modules/@emnapi/runtime": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", "license": "MIT", "optional": true, "dependencies": { @@ -560,8 +467,6 @@ }, "node_modules/@emnapi/wasi-threads": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", "license": "MIT", "optional": true, "dependencies": { @@ -634,8 +539,6 @@ }, "node_modules/@esbuild/darwin-arm64": { "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -986,8 +889,6 @@ }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -1005,8 +906,6 @@ }, "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1018,8 +917,6 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, "license": "MIT", "engines": { @@ -1028,8 +925,6 @@ }, "node_modules/@eslint/config-array": { "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1043,8 +938,6 @@ }, "node_modules/@eslint/config-helpers": { "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1053,8 +946,6 @@ }, "node_modules/@eslint/core": { "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1066,8 +957,6 @@ }, "node_modules/@eslint/eslintrc": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1090,8 +979,6 @@ }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", "engines": { @@ -1103,8 +990,6 @@ }, "node_modules/@eslint/js": { "version": "9.36.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz", - "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==", "dev": true, "license": "MIT", "engines": { @@ -1116,8 +1001,6 @@ }, "node_modules/@eslint/object-schema": { "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1126,8 +1009,6 @@ }, "node_modules/@eslint/plugin-kit": { "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1140,8 +1021,6 @@ }, "node_modules/@floating-ui/core": { "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.10" @@ -1149,8 +1028,6 @@ }, "node_modules/@floating-ui/dom": { "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", "license": "MIT", "dependencies": { "@floating-ui/core": "^1.7.3", @@ -1159,8 +1036,6 @@ }, "node_modules/@floating-ui/react-dom": { "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", - "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.7.4" @@ -1172,14 +1047,10 @@ }, "node_modules/@floating-ui/utils": { "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, "node_modules/@humanfs/core": { "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1188,8 +1059,6 @@ }, "node_modules/@humanfs/node": { "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1202,8 +1071,6 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1216,8 +1083,6 @@ }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1230,8 +1095,6 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1240,8 +1103,6 @@ }, "node_modules/@jridgewell/remapping": { "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1250,8 +1111,6 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1259,14 +1118,10 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1275,8 +1130,6 @@ }, "node_modules/@mapbox/geojson-rewind": { "version": "0.5.2", - "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", - "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", "license": "ISC", "dependencies": { "get-stream": "^6.0.1", @@ -1288,22 +1141,16 @@ }, "node_modules/@mapbox/geojson-types": { "version": "1.0.2", - "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/geojson-types/-/geojson-types-1.0.2.tgz", - "integrity": "sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw==", "license": "ISC" }, "node_modules/@mapbox/jsonlint-lines-primitives": { "version": "2.0.2", - "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", - "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", "engines": { "node": ">= 0.6" } }, "node_modules/@mapbox/mapbox-gl-supported": { "version": "1.5.0", - "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.5.0.tgz", - "integrity": "sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==", "license": "BSD-3-Clause", "peerDependencies": { "mapbox-gl": ">=0.32.1 <2.0.0" @@ -1311,26 +1158,18 @@ }, "node_modules/@mapbox/point-geometry": { "version": "0.1.0", - "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", - "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==", "license": "ISC" }, "node_modules/@mapbox/tiny-sdf": { "version": "1.2.5", - "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/tiny-sdf/-/tiny-sdf-1.2.5.tgz", - "integrity": "sha512-cD8A/zJlm6fdJOk6DqPUV8mcpyJkRz2x2R+/fYcWDYG3oWbG7/L7Yl/WqQ1VZCjnL9OTIMAn6c+BC5Eru4sQEw==", "license": "BSD-2-Clause" }, "node_modules/@mapbox/unitbezier": { "version": "0.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz", - "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==", "license": "BSD-2-Clause" }, "node_modules/@mapbox/vector-tile": { "version": "1.3.1", - "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", - "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", "license": "BSD-3-Clause", "dependencies": { "@mapbox/point-geometry": "~0.1.0" @@ -1338,8 +1177,6 @@ }, "node_modules/@mapbox/whoots-js": { "version": "3.1.0", - "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", - "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", "license": "ISC", "engines": { "node": ">=6.0.0" @@ -1347,8 +1184,6 @@ }, "node_modules/@maplibre/maplibre-gl-style-spec": { "version": "20.4.0", - "resolved": "https://npm-proxy.dev.databricks.com/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.4.0.tgz", - "integrity": "sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==", "license": "ISC", "dependencies": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", @@ -1367,20 +1202,14 @@ }, "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/@mapbox/unitbezier": { "version": "0.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", - "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", "license": "BSD-2-Clause" }, "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/tinyqueue": { "version": "3.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/tinyqueue/-/tinyqueue-3.0.0.tgz", - "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", "license": "ISC" }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", - "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", "license": "MIT", "optional": true, "dependencies": { @@ -1391,8 +1220,6 @@ }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "license": "MIT", "dependencies": { @@ -1405,8 +1232,6 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "license": "MIT", "engines": { @@ -1415,8 +1240,6 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", "dependencies": { @@ -1429,8 +1252,6 @@ }, "node_modules/@oxc-project/runtime": { "version": "0.92.0", - "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.92.0.tgz", - "integrity": "sha512-Z7x2dZOmznihvdvCvLKMl+nswtOSVxS2H2ocar+U9xx6iMfTp0VGIrX6a4xB1v80IwOPC7dT1LXIJrY70Xu3Jw==", "license": "MIT", "engines": { "node": "^20.19.0 || >=22.12.0" @@ -1438,8 +1259,6 @@ }, "node_modules/@oxc-project/types": { "version": "0.93.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.93.0.tgz", - "integrity": "sha512-yNtwmWZIBtJsMr5TEfoZFDxIWV6OdScOpza/f5YxbqUMJk+j6QX3Cf3jgZShGEFYWQJ5j9mJ6jM0tZHu2J9Yrg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -1447,14 +1266,10 @@ }, "node_modules/@plotly/d3": { "version": "3.8.2", - "resolved": "https://npm-proxy.dev.databricks.com/@plotly/d3/-/d3-3.8.2.tgz", - "integrity": "sha512-wvsNmh1GYjyJfyEBPKJLTMzgf2c2bEbSIL50lmqVUi+o1NHaLPi1Lb4v7VxXXJn043BhNyrxUrWI85Q+zmjOVA==", "license": "BSD-3-Clause" }, "node_modules/@plotly/d3-sankey": { "version": "0.7.2", - "resolved": "https://npm-proxy.dev.databricks.com/@plotly/d3-sankey/-/d3-sankey-0.7.2.tgz", - "integrity": "sha512-2jdVos1N3mMp3QW0k2q1ph7Gd6j5PY1YihBrwpkFnKqO+cqtZq3AdEYUeSGXMeLsBDQYiqTVcihYfk8vr5tqhw==", "license": "BSD-3-Clause", "dependencies": { "d3-array": "1", @@ -1464,8 +1279,6 @@ }, "node_modules/@plotly/d3-sankey-circular": { "version": "0.33.1", - "resolved": "https://npm-proxy.dev.databricks.com/@plotly/d3-sankey-circular/-/d3-sankey-circular-0.33.1.tgz", - "integrity": "sha512-FgBV1HEvCr3DV7RHhDsPXyryknucxtfnLwPtCKKxdolKyTFYoLX/ibEfX39iFYIL7DYbVeRtP43dbFcrHNE+KQ==", "license": "MIT", "dependencies": { "d3-array": "^1.2.1", @@ -1476,20 +1289,14 @@ }, "node_modules/@plotly/d3-sankey-circular/node_modules/d3-array": { "version": "1.2.4", - "resolved": "https://npm-proxy.dev.databricks.com/d3-array/-/d3-array-1.2.4.tgz", - "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", "license": "BSD-3-Clause" }, "node_modules/@plotly/d3-sankey-circular/node_modules/d3-path": { "version": "1.0.9", - "resolved": "https://npm-proxy.dev.databricks.com/d3-path/-/d3-path-1.0.9.tgz", - "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", "license": "BSD-3-Clause" }, "node_modules/@plotly/d3-sankey-circular/node_modules/d3-shape": { "version": "1.3.7", - "resolved": "https://npm-proxy.dev.databricks.com/d3-shape/-/d3-shape-1.3.7.tgz", - "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", "license": "BSD-3-Clause", "dependencies": { "d3-path": "1" @@ -1497,20 +1304,14 @@ }, "node_modules/@plotly/d3-sankey/node_modules/d3-array": { "version": "1.2.4", - "resolved": "https://npm-proxy.dev.databricks.com/d3-array/-/d3-array-1.2.4.tgz", - "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", "license": "BSD-3-Clause" }, "node_modules/@plotly/d3-sankey/node_modules/d3-path": { "version": "1.0.9", - "resolved": "https://npm-proxy.dev.databricks.com/d3-path/-/d3-path-1.0.9.tgz", - "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", "license": "BSD-3-Clause" }, "node_modules/@plotly/d3-sankey/node_modules/d3-shape": { "version": "1.3.7", - "resolved": "https://npm-proxy.dev.databricks.com/d3-shape/-/d3-shape-1.3.7.tgz", - "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", "license": "BSD-3-Clause", "dependencies": { "d3-path": "1" @@ -1518,8 +1319,6 @@ }, "node_modules/@plotly/mapbox-gl": { "version": "1.13.4", - "resolved": "https://npm-proxy.dev.databricks.com/@plotly/mapbox-gl/-/mapbox-gl-1.13.4.tgz", - "integrity": "sha512-sR3/Pe5LqT/fhYgp4rT4aSFf1rTsxMbGiH6Hojc7PH36ny5Bn17iVFUjpzycafETURuFbLZUfjODO8LvSI+5zQ==", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", @@ -1551,8 +1350,6 @@ }, "node_modules/@plotly/point-cluster": { "version": "3.1.9", - "resolved": "https://npm-proxy.dev.databricks.com/@plotly/point-cluster/-/point-cluster-3.1.9.tgz", - "integrity": "sha512-MwaI6g9scKf68Orpr1pHZ597pYx9uP8UEFXLPbsCmuw3a84obwz6pnMXGc90VhgDNeNiLEdlmuK7CPo+5PIxXw==", "license": "MIT", "dependencies": { "array-bounds": "^1.0.1", @@ -1569,26 +1366,18 @@ }, "node_modules/@plotly/regl": { "version": "2.1.2", - "resolved": "https://npm-proxy.dev.databricks.com/@plotly/regl/-/regl-2.1.2.tgz", - "integrity": "sha512-Mdk+vUACbQvjd0m/1JJjOOafmkp/EpmHjISsopEz5Av44CBq7rPC05HHNbYGKVyNUF2zmEoBS/TT0pd0SPFFyw==", "license": "MIT" }, "node_modules/@radix-ui/number": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", - "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", "license": "MIT" }, "node_modules/@radix-ui/primitive": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.3" @@ -1610,8 +1399,6 @@ }, "node_modules/@radix-ui/react-collection": { "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", @@ -1636,8 +1423,6 @@ }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1651,8 +1436,6 @@ }, "node_modules/@radix-ui/react-context": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1666,8 +1449,6 @@ }, "node_modules/@radix-ui/react-direction": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1681,8 +1462,6 @@ }, "node_modules/@radix-ui/react-dismissable-layer": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -1708,8 +1487,6 @@ }, "node_modules/@radix-ui/react-dropdown-menu": { "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", - "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -1737,8 +1514,6 @@ }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1752,8 +1527,6 @@ }, "node_modules/@radix-ui/react-focus-scope": { "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", @@ -1777,8 +1550,6 @@ }, "node_modules/@radix-ui/react-id": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" @@ -1795,8 +1566,6 @@ }, "node_modules/@radix-ui/react-menu": { "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", - "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -1835,8 +1604,6 @@ }, "node_modules/@radix-ui/react-popper": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.0.0", @@ -1867,8 +1634,6 @@ }, "node_modules/@radix-ui/react-portal": { "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.3", @@ -1891,8 +1656,6 @@ }, "node_modules/@radix-ui/react-presence": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", @@ -1915,8 +1678,6 @@ }, "node_modules/@radix-ui/react-primitive": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", "license": "MIT", "dependencies": { "@radix-ui/react-slot": "1.2.3" @@ -1938,8 +1699,6 @@ }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -1969,8 +1728,6 @@ }, "node_modules/@radix-ui/react-select": { "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", - "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.1", @@ -2012,8 +1769,6 @@ }, "node_modules/@radix-ui/react-slot": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -2030,8 +1785,6 @@ }, "node_modules/@radix-ui/react-tooltip": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", - "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -2064,8 +1817,6 @@ }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -2079,8 +1830,6 @@ }, "node_modules/@radix-ui/react-use-controllable-state": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", "license": "MIT", "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", @@ -2098,8 +1847,6 @@ }, "node_modules/@radix-ui/react-use-effect-event": { "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" @@ -2116,8 +1863,6 @@ }, "node_modules/@radix-ui/react-use-escape-keydown": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", "license": "MIT", "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" @@ -2134,8 +1879,6 @@ }, "node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -2149,8 +1892,6 @@ }, "node_modules/@radix-ui/react-use-previous": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", - "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -2164,8 +1905,6 @@ }, "node_modules/@radix-ui/react-use-rect": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", "license": "MIT", "dependencies": { "@radix-ui/rect": "1.1.1" @@ -2182,8 +1921,6 @@ }, "node_modules/@radix-ui/react-use-size": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" @@ -2200,8 +1937,6 @@ }, "node_modules/@radix-ui/react-visually-hidden": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.3" @@ -2223,14 +1958,10 @@ }, "node_modules/@radix-ui/rect": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, "node_modules/@reduxjs/toolkit": { "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.10.1.tgz", - "integrity": "sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -2271,8 +2002,6 @@ }, "node_modules/@rolldown/binding-darwin-arm64": { "version": "1.0.0-beta.41", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.41.tgz", - "integrity": "sha512-XGCzqfjdk7550PlyZRTBKbypXrB7ATtXhw/+bjtxnklLQs0mKP/XkQVOKyn9qGKSlvH8I56JLYryVxl0PCvSNw==", "cpu": [ "arm64" ], @@ -2479,15 +2208,11 @@ }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.38", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz", - "integrity": "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==", "dev": true, "license": "MIT" }, "node_modules/@shikijs/core": { "version": "3.15.0", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.15.0.tgz", - "integrity": "sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg==", "dev": true, "license": "MIT", "dependencies": { @@ -2499,8 +2224,6 @@ }, "node_modules/@shikijs/engine-javascript": { "version": "3.15.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.15.0.tgz", - "integrity": "sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg==", "dev": true, "license": "MIT", "dependencies": { @@ -2511,8 +2234,6 @@ }, "node_modules/@shikijs/engine-oniguruma": { "version": "3.15.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.15.0.tgz", - "integrity": "sha512-HnqFsV11skAHvOArMZdLBZZApRSYS4LSztk2K3016Y9VCyZISnlYUYsL2hzlS7tPqKHvNqmI5JSUJZprXloMvA==", "dev": true, "license": "MIT", "dependencies": { @@ -2522,8 +2243,6 @@ }, "node_modules/@shikijs/langs": { "version": "3.15.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.15.0.tgz", - "integrity": "sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A==", "dev": true, "license": "MIT", "dependencies": { @@ -2532,8 +2251,6 @@ }, "node_modules/@shikijs/themes": { "version": "3.15.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.15.0.tgz", - "integrity": "sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2542,8 +2259,6 @@ }, "node_modules/@shikijs/types": { "version": "3.15.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.15.0.tgz", - "integrity": "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw==", "dev": true, "license": "MIT", "dependencies": { @@ -2553,27 +2268,19 @@ }, "node_modules/@shikijs/vscode-textmate": { "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", - "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", "dev": true, "license": "MIT" }, "node_modules/@standard-schema/spec": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", "license": "MIT" }, "node_modules/@standard-schema/utils": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", - "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, "node_modules/@tailwindcss/node": { "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", - "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==", "dev": true, "license": "MIT", "dependencies": { @@ -2588,8 +2295,6 @@ }, "node_modules/@tailwindcss/oxide": { "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz", - "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==", "dev": true, "license": "MIT", "engines": { @@ -2629,8 +2334,6 @@ }, "node_modules/@tailwindcss/oxide-darwin-arm64": { "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz", - "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==", "cpu": [ "arm64" ], @@ -2793,6 +2496,66 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.6.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.6.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.0.7", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.1.17", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz", @@ -2829,8 +2592,6 @@ }, "node_modules/@tailwindcss/postcss": { "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.17.tgz", - "integrity": "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==", "dev": true, "license": "MIT", "dependencies": { @@ -2843,8 +2604,6 @@ }, "node_modules/@tanstack/history": { "version": "1.133.19", - "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.133.19.tgz", - "integrity": "sha512-Y866qBVVprdQkmO0/W1AFBI8tiQy398vFeIwP+VrRWCOzs3VecxSVzAvaOM4iHfkJz81fFAZMhLLjDVoPikD+w==", "license": "MIT", "engines": { "node": ">=12" @@ -2856,8 +2615,6 @@ }, "node_modules/@tanstack/react-router": { "version": "1.133.22", - "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.133.22.tgz", - "integrity": "sha512-0tg2yoXVMvvgR3UdOhEX9ICmgZ/Ou/I8VOl07exSYEJYfyCr5nhtB/62F9NGbuUZVrJnCzc8Rz0e4/MYU18pIg==", "license": "MIT", "dependencies": { "@tanstack/history": "1.133.19", @@ -2881,8 +2638,6 @@ }, "node_modules/@tanstack/react-router-devtools": { "version": "1.133.22", - "resolved": "https://registry.npmjs.org/@tanstack/react-router-devtools/-/react-router-devtools-1.133.22.tgz", - "integrity": "sha512-YG498dyttY7yszEGo0iE4S3ymNrX+PSWXbP7zy94RhLf3mizupInxlKaypxhIU16toKiyOQzgFgOqi6v4RqfEQ==", "license": "MIT", "dependencies": { "@tanstack/router-devtools-core": "1.133.22", @@ -2903,8 +2658,6 @@ }, "node_modules/@tanstack/react-store": { "version": "0.7.7", - "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.7.7.tgz", - "integrity": "sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg==", "license": "MIT", "dependencies": { "@tanstack/store": "0.7.7", @@ -2921,8 +2674,6 @@ }, "node_modules/@tanstack/react-table": { "version": "8.21.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", - "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", "license": "MIT", "dependencies": { "@tanstack/table-core": "8.21.3" @@ -2941,8 +2692,6 @@ }, "node_modules/@tanstack/router-cli": { "version": "1.133.20", - "resolved": "https://registry.npmjs.org/@tanstack/router-cli/-/router-cli-1.133.20.tgz", - "integrity": "sha512-XFghXTGUDzBhLbe5UWikLDbcAcuDfqWtlJvyVhDl7rYV7Pvkdb8hGgbxsriUpaVKPx5nmud8JGINIW56lQUTyA==", "dev": true, "license": "MIT", "dependencies": { @@ -2963,8 +2712,6 @@ }, "node_modules/@tanstack/router-core": { "version": "1.133.20", - "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.133.20.tgz", - "integrity": "sha512-cO8E6XA0vMX2BaPZck9kfgXK76e6Lqo13GmXEYxtXshmW8cIlgcLHhBDKnI/sCjIy9OPY2sV1qrGHtcxJy/4ew==", "license": "MIT", "dependencies": { "@tanstack/history": "1.133.19", @@ -2985,8 +2732,6 @@ }, "node_modules/@tanstack/router-devtools-core": { "version": "1.133.22", - "resolved": "https://registry.npmjs.org/@tanstack/router-devtools-core/-/router-devtools-core-1.133.22.tgz", - "integrity": "sha512-Pcpyrd3rlNA6C1jnL6jy4pC/8s4PN7270RM7+krnlKex1Rk3REgQ5LXAaAJJxOXS2coY14tiQtfQS3gx+H3b4w==", "license": "MIT", "dependencies": { "clsx": "^2.1.1", @@ -3014,8 +2759,6 @@ }, "node_modules/@tanstack/router-generator": { "version": "1.133.20", - "resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.133.20.tgz", - "integrity": "sha512-63lhmNNoVfqTgnSx5MUnEl/QBKSN6hA1sWLhZSQhCjLp9lrWbCXM8l9QpG3Tgzq/LdX7jjDMf783sUL4p4NbYw==", "license": "MIT", "dependencies": { "@tanstack/router-core": "1.133.20", @@ -3037,8 +2780,6 @@ }, "node_modules/@tanstack/router-plugin": { "version": "1.133.22", - "resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.133.22.tgz", - "integrity": "sha512-VVUazrxqFyon9bFSFY2mysgTbQAH5BV8kP8Gq1IHd7AxlboRW9tnj6TQcy8KGgG/KPCbKB9CFZtvSheKqrAVQg==", "license": "MIT", "dependencies": { "@babel/core": "^7.27.7", @@ -3090,8 +2831,6 @@ }, "node_modules/@tanstack/router-utils": { "version": "1.133.19", - "resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.133.19.tgz", - "integrity": "sha512-WEp5D2gPxvlLDRXwD/fV7RXjYtqaqJNXKB/L6OyZEbT+9BG/Ib2d7oG9GSUZNNMGPGYAlhBUOi3xutySsk6rxA==", "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", @@ -3113,8 +2852,6 @@ }, "node_modules/@tanstack/store": { "version": "0.7.7", - "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.7.tgz", - "integrity": "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==", "license": "MIT", "funding": { "type": "github", @@ -3123,8 +2860,6 @@ }, "node_modules/@tanstack/table-core": { "version": "8.21.3", - "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", - "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", "license": "MIT", "engines": { "node": ">=12" @@ -3136,8 +2871,6 @@ }, "node_modules/@tanstack/virtual-file-routes": { "version": "1.133.19", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.133.19.tgz", - "integrity": "sha512-IKwZENsK7owmW1Lm5FhuHegY/SyQ8KqtL/7mTSnzoKJgfzhrrf9qwKB1rmkKkt+svUuy/Zw3uVEpZtUzQruWtA==", "license": "MIT", "engines": { "node": ">=12" @@ -3149,8 +2882,6 @@ }, "node_modules/@turf/area": { "version": "7.3.5", - "resolved": "https://npm-proxy.dev.databricks.com/@turf/area/-/area-7.3.5.tgz", - "integrity": "sha512-sSn80wPT7XfBIDN3vurCPxhk9W4U8ozS/XImSqeLN8qveTICOxzZkhsGDMp0CuncaN+plWut4a2TdNM7mzZB6Q==", "license": "MIT", "dependencies": { "@turf/helpers": "7.3.5", @@ -3164,8 +2895,6 @@ }, "node_modules/@turf/bbox": { "version": "7.3.5", - "resolved": "https://npm-proxy.dev.databricks.com/@turf/bbox/-/bbox-7.3.5.tgz", - "integrity": "sha512-oG1ya/HtBjAIg4TimbWx+nOYPbY0bCvt82Bq8tm6sBw3qqtbOyRSfDz79Sq90TnH7DXJprJ1qnVGKNtZ6jemfw==", "license": "MIT", "dependencies": { "@turf/helpers": "7.3.5", @@ -3179,8 +2908,6 @@ }, "node_modules/@turf/centroid": { "version": "7.3.5", - "resolved": "https://npm-proxy.dev.databricks.com/@turf/centroid/-/centroid-7.3.5.tgz", - "integrity": "sha512-hkWaqwGFdOn6Tf0EWfn2yn1XZ1FWE1h2C5ZWstDMu/FxYO5DB+YjlmOFPl4K6SmSOEgdV07eK2vDCyPeTHqKGA==", "license": "MIT", "dependencies": { "@turf/helpers": "7.3.5", @@ -3194,8 +2921,6 @@ }, "node_modules/@turf/helpers": { "version": "7.3.5", - "resolved": "https://npm-proxy.dev.databricks.com/@turf/helpers/-/helpers-7.3.5.tgz", - "integrity": "sha512-E/NMGV5MwbjjP7AJXBtsanC3yY8N2MQ87IGdIgkB2ji5AtBpwnH4L3gEqpYN4RlCJJWbLbzO91BbKv2waUd0eg==", "license": "MIT", "dependencies": { "@types/geojson": "^7946.0.10", @@ -3207,8 +2932,6 @@ }, "node_modules/@turf/meta": { "version": "7.3.5", - "resolved": "https://npm-proxy.dev.databricks.com/@turf/meta/-/meta-7.3.5.tgz", - "integrity": "sha512-r+ohqxoyqeigFB0oFrQx/YEHIkOKqcKpCjvZkvZs7Tkv+IFco5MezAd2zd4rzK+0DfFgDP3KpJc7HqrYjvEjhg==", "license": "MIT", "dependencies": { "@turf/helpers": "7.3.5", @@ -3221,8 +2944,6 @@ }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "license": "MIT", "optional": true, "dependencies": { @@ -3231,8 +2952,6 @@ }, "node_modules/@types/babel__core": { "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "license": "MIT", "dependencies": { @@ -3245,8 +2964,6 @@ }, "node_modules/@types/babel__generator": { "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "dev": true, "license": "MIT", "dependencies": { @@ -3255,8 +2972,6 @@ }, "node_modules/@types/babel__template": { "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, "license": "MIT", "dependencies": { @@ -3266,8 +2981,6 @@ }, "node_modules/@types/babel__traverse": { "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3276,26 +2989,18 @@ }, "node_modules/@types/d3-array": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", - "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", "license": "MIT" }, "node_modules/@types/d3-color": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", "license": "MIT" }, "node_modules/@types/d3-ease": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", "license": "MIT" }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", "license": "MIT", "dependencies": { "@types/d3-color": "*" @@ -3303,14 +3008,10 @@ }, "node_modules/@types/d3-path": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", "license": "MIT" }, "node_modules/@types/d3-scale": { "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", "license": "MIT", "dependencies": { "@types/d3-time": "*" @@ -3318,8 +3019,6 @@ }, "node_modules/@types/d3-shape": { "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", "license": "MIT", "dependencies": { "@types/d3-path": "*" @@ -3327,33 +3026,23 @@ }, "node_modules/@types/d3-time": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", "license": "MIT" }, "node_modules/@types/d3-timer": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, "node_modules/@types/geojson": { "version": "7946.0.16", - "resolved": "https://npm-proxy.dev.databricks.com/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "license": "MIT" }, "node_modules/@types/geojson-vt": { "version": "3.2.5", - "resolved": "https://npm-proxy.dev.databricks.com/@types/geojson-vt/-/geojson-vt-3.2.5.tgz", - "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==", "license": "MIT", "dependencies": { "@types/geojson": "*" @@ -3361,8 +3050,6 @@ }, "node_modules/@types/hast": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3371,21 +3058,15 @@ }, "node_modules/@types/json-schema": { "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, "license": "MIT" }, "node_modules/@types/mapbox__point-geometry": { "version": "0.1.4", - "resolved": "https://npm-proxy.dev.databricks.com/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", - "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==", "license": "MIT" }, "node_modules/@types/mapbox__vector-tile": { "version": "1.3.4", - "resolved": "https://npm-proxy.dev.databricks.com/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz", - "integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==", "license": "MIT", "dependencies": { "@types/geojson": "*", @@ -3395,8 +3076,6 @@ }, "node_modules/@types/mdast": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", "dev": true, "license": "MIT", "dependencies": { @@ -3405,8 +3084,6 @@ }, "node_modules/@types/node": { "version": "24.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.0.tgz", - "integrity": "sha512-F1CBxgqwOMc4GKJ7eY22hWhBVQuMYTtqI8L0FcszYcpYX0fzfDGpez22Xau8Mgm7O9fI+zA/TYIdq3tGWfweBA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -3415,21 +3092,15 @@ }, "node_modules/@types/pbf": { "version": "3.0.5", - "resolved": "https://npm-proxy.dev.databricks.com/@types/pbf/-/pbf-3.0.5.tgz", - "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==", "license": "MIT" }, "node_modules/@types/plotly.js": { "version": "3.0.10", - "resolved": "https://npm-proxy.dev.databricks.com/@types/plotly.js/-/plotly.js-3.0.10.tgz", - "integrity": "sha512-q+MgO4aajC2HrO7FllTYWzrpdfbTjboSMfjkz/aXKjg1v7HNo1zMEFfAW7quKfk6SL+bH74A5ThBEps/7hZxOA==", "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", - "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -3438,8 +3109,6 @@ }, "node_modules/@types/react-dom": { "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", - "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", "peerDependencies": { @@ -3448,8 +3117,6 @@ }, "node_modules/@types/react-plotly.js": { "version": "2.6.4", - "resolved": "https://npm-proxy.dev.databricks.com/@types/react-plotly.js/-/react-plotly.js-2.6.4.tgz", - "integrity": "sha512-AU6w1u3qEGM0NmBA69PaOgNc0KPFA/+qkH6Uu9EBTJ45/WYOUoXi9AF5O15PRM2klpHSiHAAs4WnlI+OZAFmUA==", "dev": true, "license": "MIT", "dependencies": { @@ -3459,8 +3126,6 @@ }, "node_modules/@types/supercluster": { "version": "7.1.3", - "resolved": "https://npm-proxy.dev.databricks.com/@types/supercluster/-/supercluster-7.1.3.tgz", - "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", "license": "MIT", "dependencies": { "@types/geojson": "*" @@ -3468,21 +3133,15 @@ }, "node_modules/@types/unist": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "dev": true, "license": "MIT" }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", - "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz", - "integrity": "sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==", "dev": true, "license": "MIT", "dependencies": { @@ -3511,8 +3170,6 @@ }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -3521,8 +3178,6 @@ }, "node_modules/@typescript-eslint/parser": { "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.45.0.tgz", - "integrity": "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3546,8 +3201,6 @@ }, "node_modules/@typescript-eslint/project-service": { "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.45.0.tgz", - "integrity": "sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg==", "dev": true, "license": "MIT", "dependencies": { @@ -3568,8 +3221,6 @@ }, "node_modules/@typescript-eslint/scope-manager": { "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.45.0.tgz", - "integrity": "sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA==", "dev": true, "license": "MIT", "dependencies": { @@ -3586,8 +3237,6 @@ }, "node_modules/@typescript-eslint/tsconfig-utils": { "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.45.0.tgz", - "integrity": "sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w==", "dev": true, "license": "MIT", "engines": { @@ -3603,8 +3252,6 @@ }, "node_modules/@typescript-eslint/type-utils": { "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.45.0.tgz", - "integrity": "sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A==", "dev": true, "license": "MIT", "dependencies": { @@ -3628,8 +3275,6 @@ }, "node_modules/@typescript-eslint/types": { "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.45.0.tgz", - "integrity": "sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA==", "dev": true, "license": "MIT", "engines": { @@ -3642,8 +3287,6 @@ }, "node_modules/@typescript-eslint/typescript-estree": { "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.45.0.tgz", - "integrity": "sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA==", "dev": true, "license": "MIT", "dependencies": { @@ -3671,8 +3314,6 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3681,8 +3322,6 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { @@ -3697,8 +3336,6 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -3710,8 +3347,6 @@ }, "node_modules/@typescript-eslint/utils": { "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.45.0.tgz", - "integrity": "sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg==", "dev": true, "license": "MIT", "dependencies": { @@ -3734,8 +3369,6 @@ }, "node_modules/@typescript-eslint/visitor-keys": { "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.45.0.tgz", - "integrity": "sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==", "dev": true, "license": "MIT", "dependencies": { @@ -3752,15 +3385,11 @@ }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true, "license": "ISC" }, "node_modules/@vitejs/plugin-react": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.4.tgz", - "integrity": "sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==", "dev": true, "license": "MIT", "dependencies": { @@ -3780,14 +3409,10 @@ }, "node_modules/abs-svg-path": { "version": "0.1.1", - "resolved": "https://npm-proxy.dev.databricks.com/abs-svg-path/-/abs-svg-path-0.1.1.tgz", - "integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==", "license": "MIT" }, "node_modules/acorn": { "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -3798,8 +3423,6 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -3808,8 +3431,6 @@ }, "node_modules/ajv": { "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", "dependencies": { @@ -3825,8 +3446,6 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { @@ -3841,8 +3460,6 @@ }, "node_modules/ansis": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", - "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", "license": "ISC", "engines": { "node": ">=14" @@ -3850,8 +3467,6 @@ }, "node_modules/anymatch": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -3863,15 +3478,11 @@ }, "node_modules/argparse": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, "license": "Python-2.0" }, "node_modules/aria-hidden": { "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -3882,14 +3493,10 @@ }, "node_modules/array-bounds": { "version": "1.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/array-bounds/-/array-bounds-1.0.1.tgz", - "integrity": "sha512-8wdW3ZGk6UjMPJx/glyEt0sLzzwAE1bhToPsO1W2pbpR2gULyxe3BjSiuJFheP50T/GgODVPz2fuMUmIywt8cQ==", "license": "MIT" }, "node_modules/array-find-index": { "version": "1.0.2", - "resolved": "https://npm-proxy.dev.databricks.com/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3897,8 +3504,6 @@ }, "node_modules/array-normalize": { "version": "1.1.4", - "resolved": "https://npm-proxy.dev.databricks.com/array-normalize/-/array-normalize-1.1.4.tgz", - "integrity": "sha512-fCp0wKFLjvSPmCn4F5Tiw4M3lpMZoHlCjfcs7nNzuj3vqQQ1/a8cgB9DXcpDSn18c+coLnaW7rqfcYCvKbyJXg==", "license": "MIT", "dependencies": { "array-bounds": "^1.0.0" @@ -3906,14 +3511,10 @@ }, "node_modules/array-range": { "version": "1.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/array-range/-/array-range-1.0.1.tgz", - "integrity": "sha512-shdaI1zT3CVNL2hnx9c0JMc0ZogGaxDs5e85akgHWKYa0yVbIyp06Ind3dVkTj/uuFrzaHBOyqFzo+VV6aXgtA==", "license": "MIT" }, "node_modules/ast-types": { "version": "0.16.1", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", - "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", "license": "MIT", "dependencies": { "tslib": "^2.0.1" @@ -3924,8 +3525,6 @@ }, "node_modules/autoprefixer": { "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", "dev": true, "funding": [ { @@ -3962,8 +3561,6 @@ }, "node_modules/babel-dead-code-elimination": { "version": "1.0.10", - "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.10.tgz", - "integrity": "sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA==", "license": "MIT", "dependencies": { "@babel/core": "^7.23.7", @@ -3974,15 +3571,11 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/base64-arraybuffer": { "version": "1.0.2", - "resolved": "https://npm-proxy.dev.databricks.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", - "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", "license": "MIT", "engines": { "node": ">= 0.6.0" @@ -3990,8 +3583,6 @@ }, "node_modules/baseline-browser-mapping": { "version": "2.8.16", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", - "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" @@ -3999,8 +3590,6 @@ }, "node_modules/binary-extensions": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "license": "MIT", "engines": { "node": ">=8" @@ -4011,26 +3600,18 @@ }, "node_modules/binary-search-bounds": { "version": "2.0.5", - "resolved": "https://npm-proxy.dev.databricks.com/binary-search-bounds/-/binary-search-bounds-2.0.5.tgz", - "integrity": "sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==", "license": "MIT" }, "node_modules/bit-twiddle": { "version": "1.0.2", - "resolved": "https://npm-proxy.dev.databricks.com/bit-twiddle/-/bit-twiddle-1.0.2.tgz", - "integrity": "sha512-B9UhK0DKFZhoTFcfvAzhqsjStvGJp9vYWf3+6SNTtdSQnvIgfkHbgHrg/e4+TH71N2GDu8tpmCVoyfrL1d7ntA==", "license": "MIT" }, "node_modules/bitmap-sdf": { "version": "1.0.4", - "resolved": "https://npm-proxy.dev.databricks.com/bitmap-sdf/-/bitmap-sdf-1.0.4.tgz", - "integrity": "sha512-1G3U4n5JE6RAiALMxu0p1XmeZkTeCwGKykzsLTCqVzfSDaN6S7fKnkIkfejogz+iwqBWc0UYAIKnKHNN7pSfDg==", "license": "MIT" }, "node_modules/bl": { "version": "2.2.1", - "resolved": "https://npm-proxy.dev.databricks.com/bl/-/bl-2.2.1.tgz", - "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", "license": "MIT", "dependencies": { "readable-stream": "^2.3.5", @@ -4039,8 +3620,6 @@ }, "node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -4050,8 +3629,6 @@ }, "node_modules/braces": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -4062,8 +3639,6 @@ }, "node_modules/browserslist": { "version": "4.26.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", - "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", "funding": [ { "type": "opencollective", @@ -4095,14 +3670,10 @@ }, "node_modules/buffer-from": { "version": "1.1.2", - "resolved": "https://npm-proxy.dev.databricks.com/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, "node_modules/callsites": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", "engines": { @@ -4111,8 +3682,6 @@ }, "node_modules/caniuse-lite": { "version": "1.0.30001750", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz", - "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==", "funding": [ { "type": "opencollective", @@ -4131,8 +3700,6 @@ }, "node_modules/canvas-fit": { "version": "1.5.0", - "resolved": "https://npm-proxy.dev.databricks.com/canvas-fit/-/canvas-fit-1.5.0.tgz", - "integrity": "sha512-onIcjRpz69/Hx5bB5HGbYKUF2uC6QT6Gp+pfpGm3A7mPfcluSLV5v4Zu+oflDUwLdUw0rLIBhUbi0v8hM4FJQQ==", "license": "MIT", "dependencies": { "element-size": "^1.1.1" @@ -4140,8 +3707,6 @@ }, "node_modules/ccount": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", "dev": true, "license": "MIT", "funding": { @@ -4151,8 +3716,6 @@ }, "node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { @@ -4168,8 +3731,6 @@ }, "node_modules/character-entities-html4": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", "dev": true, "license": "MIT", "funding": { @@ -4179,8 +3740,6 @@ }, "node_modules/character-entities-legacy": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", "dev": true, "license": "MIT", "funding": { @@ -4190,8 +3749,6 @@ }, "node_modules/chokidar": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -4214,8 +3771,6 @@ }, "node_modules/chokidar/node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -4226,14 +3781,10 @@ }, "node_modules/clamp": { "version": "1.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/clamp/-/clamp-1.0.1.tgz", - "integrity": "sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA==", "license": "MIT" }, "node_modules/class-variance-authority": { "version": "0.7.1", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", "license": "Apache-2.0", "dependencies": { "clsx": "^2.1.1" @@ -4244,8 +3795,6 @@ }, "node_modules/cliui": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "license": "ISC", "dependencies": { @@ -4259,8 +3808,6 @@ }, "node_modules/cliui/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -4269,15 +3816,11 @@ }, "node_modules/cliui/node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, "node_modules/cliui/node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -4291,8 +3834,6 @@ }, "node_modules/cliui/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -4304,8 +3845,6 @@ }, "node_modules/cliui/node_modules/wrap-ansi": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4322,8 +3861,6 @@ }, "node_modules/clsx": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", "engines": { "node": ">=6" @@ -4331,8 +3868,6 @@ }, "node_modules/color-alpha": { "version": "1.0.4", - "resolved": "https://npm-proxy.dev.databricks.com/color-alpha/-/color-alpha-1.0.4.tgz", - "integrity": "sha512-lr8/t5NPozTSqli+duAN+x+no/2WaKTeWvxhHGN+aXT6AJ8vPlzLa7UriyjWak0pSC2jHol9JgjBYnnHsGha9A==", "license": "MIT", "dependencies": { "color-parse": "^1.3.8" @@ -4340,8 +3875,6 @@ }, "node_modules/color-alpha/node_modules/color-parse": { "version": "1.4.3", - "resolved": "https://npm-proxy.dev.databricks.com/color-parse/-/color-parse-1.4.3.tgz", - "integrity": "sha512-BADfVl/FHkQkyo8sRBwMYBqemqsgnu7JZAwUgvBvuwwuNUZAhSvLTbsEErS5bQXzOjDR0dWzJ4vXN2Q+QoPx0A==", "license": "MIT", "dependencies": { "color-name": "^1.0.0" @@ -4349,8 +3882,6 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4362,8 +3893,6 @@ }, "node_modules/color-id": { "version": "1.1.0", - "resolved": "https://npm-proxy.dev.databricks.com/color-id/-/color-id-1.1.0.tgz", - "integrity": "sha512-2iRtAn6dC/6/G7bBIo0uupVrIne1NsQJvJxZOBCzQOfk7jRq97feaDZ3RdzuHakRXXnHGNwglto3pqtRx1sX0g==", "license": "MIT", "dependencies": { "clamp": "^1.0.1" @@ -4371,14 +3900,10 @@ }, "node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, "node_modules/color-normalize": { "version": "1.5.0", - "resolved": "https://npm-proxy.dev.databricks.com/color-normalize/-/color-normalize-1.5.0.tgz", - "integrity": "sha512-rUT/HDXMr6RFffrR53oX3HGWkDOP9goSAQGBkUaAYKjOE2JxozccdGyufageWDlInRAjm/jYPrf/Y38oa+7obw==", "license": "MIT", "dependencies": { "clamp": "^1.0.1", @@ -4388,8 +3913,6 @@ }, "node_modules/color-normalize/node_modules/color-parse": { "version": "1.4.3", - "resolved": "https://npm-proxy.dev.databricks.com/color-parse/-/color-parse-1.4.3.tgz", - "integrity": "sha512-BADfVl/FHkQkyo8sRBwMYBqemqsgnu7JZAwUgvBvuwwuNUZAhSvLTbsEErS5bQXzOjDR0dWzJ4vXN2Q+QoPx0A==", "license": "MIT", "dependencies": { "color-name": "^1.0.0" @@ -4397,8 +3920,6 @@ }, "node_modules/color-normalize/node_modules/color-rgba": { "version": "2.4.0", - "resolved": "https://npm-proxy.dev.databricks.com/color-rgba/-/color-rgba-2.4.0.tgz", - "integrity": "sha512-Nti4qbzr/z2LbUWySr7H9dk3Rl7gZt7ihHAxlgT4Ho90EXWkjtkL1avTleu9yeGuqrt/chxTB6GKK8nZZ6V0+Q==", "license": "MIT", "dependencies": { "color-parse": "^1.4.2", @@ -4407,8 +3928,6 @@ }, "node_modules/color-parse": { "version": "2.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/color-parse/-/color-parse-2.0.0.tgz", - "integrity": "sha512-g2Z+QnWsdHLppAbrpcFWo629kLOnOPtpxYV69GCqm92gqSgyXbzlfyN3MXs0412fPBkFmiuS+rXposgBgBa6Kg==", "license": "MIT", "dependencies": { "color-name": "^1.0.0" @@ -4416,8 +3935,6 @@ }, "node_modules/color-rgba": { "version": "3.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/color-rgba/-/color-rgba-3.0.0.tgz", - "integrity": "sha512-PPwZYkEY3M2THEHHV6Y95sGUie77S7X8v+h1r6LSAPF3/LL2xJ8duUXSrkic31Nzc4odPwHgUbiX/XuTYzQHQg==", "license": "MIT", "dependencies": { "color-parse": "^2.0.0", @@ -4426,14 +3943,10 @@ }, "node_modules/color-space": { "version": "2.3.2", - "resolved": "https://npm-proxy.dev.databricks.com/color-space/-/color-space-2.3.2.tgz", - "integrity": "sha512-BcKnbOEsOarCwyoLstcoEztwT0IJxqqQkNwDuA3a65sICvvHL2yoeV13psoDFh5IuiOMnIOKdQDwB4Mk3BypiA==", "license": "Unlicense" }, "node_modules/comma-separated-tokens": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", "dev": true, "license": "MIT", "funding": { @@ -4443,21 +3956,15 @@ }, "node_modules/commander": { "version": "2.20.3", - "resolved": "https://npm-proxy.dev.databricks.com/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, "node_modules/concat-stream": { "version": "1.6.2", - "resolved": "https://npm-proxy.dev.databricks.com/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "engines": [ "node >= 0.8" ], @@ -4471,32 +3978,22 @@ }, "node_modules/convert-source-map": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, "node_modules/cookie-es": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.0.tgz", - "integrity": "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==", "license": "MIT" }, "node_modules/core-util-is": { "version": "1.0.3", - "resolved": "https://npm-proxy.dev.databricks.com/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, "node_modules/country-regex": { "version": "1.1.0", - "resolved": "https://npm-proxy.dev.databricks.com/country-regex/-/country-regex-1.1.0.tgz", - "integrity": "sha512-iSPlClZP8vX7MC3/u6s3lrDuoQyhQukh5LyABJ3hvfzbQ3Yyayd4fp04zjLnfi267B/B2FkumcWWgrbban7sSA==", "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -4510,8 +4007,6 @@ }, "node_modules/css-font": { "version": "1.2.0", - "resolved": "https://npm-proxy.dev.databricks.com/css-font/-/css-font-1.2.0.tgz", - "integrity": "sha512-V4U4Wps4dPDACJ4WpgofJ2RT5Yqwe1lEH6wlOOaIxMi0gTjdIijsc5FmxQlZ7ZZyKQkkutqqvULOp07l9c7ssA==", "license": "MIT", "dependencies": { "css-font-size-keywords": "^1.0.0", @@ -4527,56 +4022,38 @@ }, "node_modules/css-font-size-keywords": { "version": "1.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/css-font-size-keywords/-/css-font-size-keywords-1.0.0.tgz", - "integrity": "sha512-Q+svMDbMlelgCfH/RVDKtTDaf5021O486ZThQPIpahnIjUkMUslC+WuOQSWTgGSrNCH08Y7tYNEmmy0hkfMI8Q==", "license": "MIT" }, "node_modules/css-font-stretch-keywords": { "version": "1.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/css-font-stretch-keywords/-/css-font-stretch-keywords-1.0.1.tgz", - "integrity": "sha512-KmugPO2BNqoyp9zmBIUGwt58UQSfyk1X5DbOlkb2pckDXFSAfjsD5wenb88fNrD6fvS+vu90a/tsPpb9vb0SLg==", "license": "MIT" }, "node_modules/css-font-style-keywords": { "version": "1.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/css-font-style-keywords/-/css-font-style-keywords-1.0.1.tgz", - "integrity": "sha512-0Fn0aTpcDktnR1RzaBYorIxQily85M2KXRpzmxQPgh8pxUN9Fcn00I8u9I3grNr1QXVgCl9T5Imx0ZwKU973Vg==", "license": "MIT" }, "node_modules/css-font-weight-keywords": { "version": "1.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/css-font-weight-keywords/-/css-font-weight-keywords-1.0.0.tgz", - "integrity": "sha512-5So8/NH+oDD+EzsnF4iaG4ZFHQ3vaViePkL1ZbZ5iC/KrsCY+WHq/lvOgrtmuOQ9pBBZ1ADGpaf+A4lj1Z9eYA==", "license": "MIT" }, "node_modules/css-global-keywords": { "version": "1.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/css-global-keywords/-/css-global-keywords-1.0.1.tgz", - "integrity": "sha512-X1xgQhkZ9n94WDwntqst5D/FKkmiU0GlJSFZSV3kLvyJ1WC5VeyoXDOuleUD+SIuH9C7W05is++0Woh0CGfKjQ==", "license": "MIT" }, "node_modules/css-system-font-keywords": { "version": "1.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/css-system-font-keywords/-/css-system-font-keywords-1.0.0.tgz", - "integrity": "sha512-1umTtVd/fXS25ftfjB71eASCrYhilmEsvDEI6wG/QplnmlfmVM5HkZ/ZX46DT5K3eblFPgLUHt5BRCb0YXkSFA==", "license": "MIT" }, "node_modules/csscolorparser": { "version": "1.0.3", - "resolved": "https://npm-proxy.dev.databricks.com/csscolorparser/-/csscolorparser-1.0.3.tgz", - "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==", "license": "MIT" }, "node_modules/csstype": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, "node_modules/d": { "version": "1.0.2", - "resolved": "https://npm-proxy.dev.databricks.com/d/-/d-1.0.2.tgz", - "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", "license": "ISC", "dependencies": { "es5-ext": "^0.10.64", @@ -4588,8 +4065,6 @@ }, "node_modules/d3-array": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", "license": "ISC", "dependencies": { "internmap": "1 - 2" @@ -4600,14 +4075,10 @@ }, "node_modules/d3-collection": { "version": "1.0.7", - "resolved": "https://npm-proxy.dev.databricks.com/d3-collection/-/d3-collection-1.0.7.tgz", - "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==", "license": "BSD-3-Clause" }, "node_modules/d3-color": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", "license": "ISC", "engines": { "node": ">=12" @@ -4615,14 +4086,10 @@ }, "node_modules/d3-dispatch": { "version": "1.0.6", - "resolved": "https://npm-proxy.dev.databricks.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz", - "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==", "license": "BSD-3-Clause" }, "node_modules/d3-ease": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", "license": "BSD-3-Clause", "engines": { "node": ">=12" @@ -4630,8 +4097,6 @@ }, "node_modules/d3-force": { "version": "1.2.1", - "resolved": "https://npm-proxy.dev.databricks.com/d3-force/-/d3-force-1.2.1.tgz", - "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", "license": "BSD-3-Clause", "dependencies": { "d3-collection": "1", @@ -4642,14 +4107,10 @@ }, "node_modules/d3-force/node_modules/d3-timer": { "version": "1.0.10", - "resolved": "https://npm-proxy.dev.databricks.com/d3-timer/-/d3-timer-1.0.10.tgz", - "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==", "license": "BSD-3-Clause" }, "node_modules/d3-format": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", "license": "ISC", "engines": { "node": ">=12" @@ -4657,8 +4118,6 @@ }, "node_modules/d3-geo": { "version": "1.12.1", - "resolved": "https://npm-proxy.dev.databricks.com/d3-geo/-/d3-geo-1.12.1.tgz", - "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", "license": "BSD-3-Clause", "dependencies": { "d3-array": "1" @@ -4666,8 +4125,6 @@ }, "node_modules/d3-geo-projection": { "version": "2.9.0", - "resolved": "https://npm-proxy.dev.databricks.com/d3-geo-projection/-/d3-geo-projection-2.9.0.tgz", - "integrity": "sha512-ZULvK/zBn87of5rWAfFMc9mJOipeSo57O+BBitsKIXmU4rTVAnX1kSsJkE0R+TxY8pGNoM1nbyRRE7GYHhdOEQ==", "license": "BSD-3-Clause", "dependencies": { "commander": "2", @@ -4685,26 +4142,18 @@ }, "node_modules/d3-geo-projection/node_modules/d3-array": { "version": "1.2.4", - "resolved": "https://npm-proxy.dev.databricks.com/d3-array/-/d3-array-1.2.4.tgz", - "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", "license": "BSD-3-Clause" }, "node_modules/d3-geo/node_modules/d3-array": { "version": "1.2.4", - "resolved": "https://npm-proxy.dev.databricks.com/d3-array/-/d3-array-1.2.4.tgz", - "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", "license": "BSD-3-Clause" }, "node_modules/d3-hierarchy": { "version": "1.1.9", - "resolved": "https://npm-proxy.dev.databricks.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", - "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==", "license": "BSD-3-Clause" }, "node_modules/d3-interpolate": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", "license": "ISC", "dependencies": { "d3-color": "1 - 3" @@ -4715,8 +4164,6 @@ }, "node_modules/d3-path": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", "license": "ISC", "engines": { "node": ">=12" @@ -4724,14 +4171,10 @@ }, "node_modules/d3-quadtree": { "version": "1.0.7", - "resolved": "https://npm-proxy.dev.databricks.com/d3-quadtree/-/d3-quadtree-1.0.7.tgz", - "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==", "license": "BSD-3-Clause" }, "node_modules/d3-scale": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", "license": "ISC", "dependencies": { "d3-array": "2.10.0 - 3", @@ -4746,8 +4189,6 @@ }, "node_modules/d3-shape": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", "license": "ISC", "dependencies": { "d3-path": "^3.1.0" @@ -4758,8 +4199,6 @@ }, "node_modules/d3-time": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", "license": "ISC", "dependencies": { "d3-array": "2 - 3" @@ -4770,8 +4209,6 @@ }, "node_modules/d3-time-format": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", "license": "ISC", "dependencies": { "d3-time": "1 - 3" @@ -4782,8 +4219,6 @@ }, "node_modules/d3-timer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", "license": "ISC", "engines": { "node": ">=12" @@ -4791,8 +4226,6 @@ }, "node_modules/debug": { "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4808,21 +4241,15 @@ }, "node_modules/decimal.js-light": { "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, "node_modules/deep-is": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, "node_modules/defined": { "version": "1.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/defined/-/defined-1.0.1.tgz", - "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4830,8 +4257,6 @@ }, "node_modules/dequal": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, "license": "MIT", "engines": { @@ -4840,14 +4265,10 @@ }, "node_modules/detect-kerning": { "version": "2.1.2", - "resolved": "https://npm-proxy.dev.databricks.com/detect-kerning/-/detect-kerning-2.1.2.tgz", - "integrity": "sha512-I3JIbrnKPAntNLl1I6TpSQQdQ4AutYzv/sKMFKbepawV/hlH0GmYKhUoOEMd4xqaUHT+Bm0f4127lh5qs1m1tw==", "license": "MIT" }, "node_modules/detect-libc": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -4855,14 +4276,10 @@ }, "node_modules/detect-node-es": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, "node_modules/devlop": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", "dev": true, "license": "MIT", "dependencies": { @@ -4875,8 +4292,6 @@ }, "node_modules/diff": { "version": "8.0.3", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", - "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -4884,8 +4299,6 @@ }, "node_modules/draw-svg-path": { "version": "1.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/draw-svg-path/-/draw-svg-path-1.0.0.tgz", - "integrity": "sha512-P8j3IHxcgRMcY6sDzr0QvJDLzBnJJqpTG33UZ2Pvp8rw0apCHhJCWqYprqrXjrgHnJ6tuhP1iTJSAodPDHxwkg==", "license": "MIT", "dependencies": { "abs-svg-path": "~0.1.1", @@ -4894,8 +4307,6 @@ }, "node_modules/dtype": { "version": "2.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/dtype/-/dtype-2.0.0.tgz", - "integrity": "sha512-s2YVcLKdFGS0hpFqJaTwscsyt0E8nNFdmo73Ocd81xNPj4URI4rj6D60A+vFMIw7BXWlb4yRkEwfBqcZzPGiZg==", "license": "MIT", "engines": { "node": ">= 0.8.0" @@ -4903,14 +4314,10 @@ }, "node_modules/dup": { "version": "1.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/dup/-/dup-1.0.0.tgz", - "integrity": "sha512-Bz5jxMMC0wgp23Zm15ip1x8IhYRqJvF3nFC0UInJUDkN1z4uNPk9jTnfCUJXbOGiQ1JbXLQsiV41Fb+HXcj5BA==", "license": "MIT" }, "node_modules/duplexify": { "version": "3.7.1", - "resolved": "https://npm-proxy.dev.databricks.com/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", "license": "MIT", "dependencies": { "end-of-stream": "^1.0.0", @@ -4921,26 +4328,18 @@ }, "node_modules/earcut": { "version": "2.2.4", - "resolved": "https://npm-proxy.dev.databricks.com/earcut/-/earcut-2.2.4.tgz", - "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", "license": "ISC" }, "node_modules/electron-to-chromium": { "version": "1.5.237", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz", - "integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==", "license": "ISC" }, "node_modules/element-size": { "version": "1.1.1", - "resolved": "https://npm-proxy.dev.databricks.com/element-size/-/element-size-1.1.1.tgz", - "integrity": "sha512-eaN+GMOq/Q+BIWy0ybsgpcYImjGIdNLyjLFJU4XsLHXYQao5jCNb36GyN6C2qwmDDYSfIBmKpPpr4VnBdLCsPQ==", "license": "MIT" }, "node_modules/elementary-circuits-directed-graph": { "version": "1.3.1", - "resolved": "https://npm-proxy.dev.databricks.com/elementary-circuits-directed-graph/-/elementary-circuits-directed-graph-1.3.1.tgz", - "integrity": "sha512-ZEiB5qkn2adYmpXGnJKkxT8uJHlW/mxmBpmeqawEHzPxh9HkLD4/1mFYX5l0On+f6rcPIt8/EWlRU2Vo3fX6dQ==", "license": "MIT", "dependencies": { "strongly-connected-components": "^1.0.1" @@ -4948,8 +4347,6 @@ }, "node_modules/end-of-stream": { "version": "1.4.5", - "resolved": "https://npm-proxy.dev.databricks.com/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -4957,8 +4354,6 @@ }, "node_modules/enhanced-resolve": { "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "dev": true, "license": "MIT", "dependencies": { @@ -4971,8 +4366,6 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "resolved": "https://npm-proxy.dev.databricks.com/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -4980,8 +4373,6 @@ }, "node_modules/es-toolkit": { "version": "1.42.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.42.0.tgz", - "integrity": "sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA==", "license": "MIT", "workspaces": [ "docs", @@ -4990,8 +4381,6 @@ }, "node_modules/es5-ext": { "version": "0.10.64", - "resolved": "https://npm-proxy.dev.databricks.com/es5-ext/-/es5-ext-0.10.64.tgz", - "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "hasInstallScript": true, "license": "ISC", "dependencies": { @@ -5006,8 +4395,6 @@ }, "node_modules/es6-iterator": { "version": "2.0.3", - "resolved": "https://npm-proxy.dev.databricks.com/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", "license": "MIT", "dependencies": { "d": "1", @@ -5017,8 +4404,6 @@ }, "node_modules/es6-symbol": { "version": "3.1.4", - "resolved": "https://npm-proxy.dev.databricks.com/es6-symbol/-/es6-symbol-3.1.4.tgz", - "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", "license": "ISC", "dependencies": { "d": "^1.0.2", @@ -5030,8 +4415,6 @@ }, "node_modules/es6-weak-map": { "version": "2.0.3", - "resolved": "https://npm-proxy.dev.databricks.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz", - "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", "license": "ISC", "dependencies": { "d": "1", @@ -5042,8 +4425,6 @@ }, "node_modules/esbuild": { "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -5083,8 +4464,6 @@ }, "node_modules/escalade": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", "engines": { "node": ">=6" @@ -5092,8 +4471,6 @@ }, "node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { @@ -5105,8 +4482,6 @@ }, "node_modules/escodegen": { "version": "2.1.0", - "resolved": "https://npm-proxy.dev.databricks.com/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", "license": "BSD-2-Clause", "dependencies": { "esprima": "^4.0.1", @@ -5126,8 +4501,6 @@ }, "node_modules/escodegen/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://npm-proxy.dev.databricks.com/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", "optional": true, "engines": { @@ -5136,8 +4509,6 @@ }, "node_modules/eslint": { "version": "9.36.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz", - "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5197,8 +4568,6 @@ }, "node_modules/eslint-plugin-react-hooks": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", "dev": true, "license": "MIT", "engines": { @@ -5210,8 +4579,6 @@ }, "node_modules/eslint-plugin-react-refresh": { "version": "0.4.22", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.22.tgz", - "integrity": "sha512-atkAG6QaJMGoTLc4MDAP+rqZcfwQuTIh2IqHWFLy2TEjxr0MOK+5BSG4RzL2564AAPpZkDRsZXAUz68kjnU6Ug==", "dev": true, "license": "MIT", "peerDependencies": { @@ -5220,8 +4587,6 @@ }, "node_modules/eslint-scope": { "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5237,8 +4602,6 @@ }, "node_modules/eslint-visitor-keys": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5250,8 +4613,6 @@ }, "node_modules/esniff": { "version": "2.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/esniff/-/esniff-2.0.1.tgz", - "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", "license": "ISC", "dependencies": { "d": "^1.0.1", @@ -5265,8 +4626,6 @@ }, "node_modules/espree": { "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5283,8 +4642,6 @@ }, "node_modules/esprima": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -5296,8 +4653,6 @@ }, "node_modules/esquery": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5309,8 +4664,6 @@ }, "node_modules/esrecurse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5322,8 +4675,6 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -5331,8 +4682,6 @@ }, "node_modules/esutils": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -5340,8 +4689,6 @@ }, "node_modules/event-emitter": { "version": "0.3.5", - "resolved": "https://npm-proxy.dev.databricks.com/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", "license": "MIT", "dependencies": { "d": "1", @@ -5350,14 +4697,10 @@ }, "node_modules/eventemitter3": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, "node_modules/events": { "version": "3.3.0", - "resolved": "https://npm-proxy.dev.databricks.com/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "license": "MIT", "engines": { "node": ">=0.8.x" @@ -5365,8 +4708,6 @@ }, "node_modules/ext": { "version": "1.7.0", - "resolved": "https://npm-proxy.dev.databricks.com/ext/-/ext-1.7.0.tgz", - "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", "license": "ISC", "dependencies": { "type": "^2.7.2" @@ -5374,8 +4715,6 @@ }, "node_modules/falafel": { "version": "2.2.5", - "resolved": "https://npm-proxy.dev.databricks.com/falafel/-/falafel-2.2.5.tgz", - "integrity": "sha512-HuC1qF9iTnHDnML9YZAdCDQwT0yKl/U55K4XSUXqGAA2GLoafFgWRqdAbhWJxXaYD4pyoVxAJ8wH670jMpI9DQ==", "license": "MIT", "dependencies": { "acorn": "^7.1.1", @@ -5387,8 +4726,6 @@ }, "node_modules/falafel/node_modules/acorn": { "version": "7.4.1", - "resolved": "https://npm-proxy.dev.databricks.com/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -5399,15 +4736,11 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -5423,8 +4756,6 @@ }, "node_modules/fast-glob/node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "license": "ISC", "dependencies": { @@ -5436,8 +4767,6 @@ }, "node_modules/fast-isnumeric": { "version": "1.1.4", - "resolved": "https://npm-proxy.dev.databricks.com/fast-isnumeric/-/fast-isnumeric-1.1.4.tgz", - "integrity": "sha512-1mM8qOr2LYz8zGaUdmiqRDiuue00Dxjgcb1NQR7TnhLVh6sQyngP9xvLo7Sl7LZpP/sk5eb+bcyWXw530NTBZw==", "license": "MIT", "dependencies": { "is-string-blank": "^1.0.1" @@ -5445,22 +4774,16 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, "node_modules/fastq": { "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -5469,8 +4792,6 @@ }, "node_modules/file-entry-cache": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5482,8 +4803,6 @@ }, "node_modules/fill-range": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -5494,8 +4813,6 @@ }, "node_modules/find-up": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { @@ -5511,8 +4828,6 @@ }, "node_modules/flat-cache": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { @@ -5525,15 +4840,11 @@ }, "node_modules/flatted": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, "node_modules/flatten-vertex-data": { "version": "1.0.2", - "resolved": "https://npm-proxy.dev.databricks.com/flatten-vertex-data/-/flatten-vertex-data-1.0.2.tgz", - "integrity": "sha512-BvCBFK2NZqerFTdMDgqfHBwxYWnxeCkwONsw6PvBMcUXqo8U/KDWwmXhqx1x2kLIg7DqIsJfOaJFOmlua3Lxuw==", "license": "MIT", "dependencies": { "dtype": "^2.0.0" @@ -5541,8 +4852,6 @@ }, "node_modules/font-atlas": { "version": "2.1.0", - "resolved": "https://npm-proxy.dev.databricks.com/font-atlas/-/font-atlas-2.1.0.tgz", - "integrity": "sha512-kP3AmvX+HJpW4w3d+PiPR2X6E1yvsBXt2yhuCw+yReO9F1WYhvZwx3c95DGZGwg9xYzDGrgJYa885xmVA+28Cg==", "license": "MIT", "dependencies": { "css-font": "^1.0.0" @@ -5550,8 +4859,6 @@ }, "node_modules/font-measure": { "version": "1.2.2", - "resolved": "https://npm-proxy.dev.databricks.com/font-measure/-/font-measure-1.2.2.tgz", - "integrity": "sha512-mRLEpdrWzKe9hbfaF3Qpr06TAjquuBVP5cHy4b3hyeNdjc9i0PO6HniGsX5vjL5OWv7+Bd++NiooNpT/s8BvIA==", "license": "MIT", "dependencies": { "css-font": "^1.2.0" @@ -5559,8 +4866,6 @@ }, "node_modules/fraction.js": { "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "dev": true, "license": "MIT", "engines": { @@ -5573,8 +4878,6 @@ }, "node_modules/from2": { "version": "2.3.0", - "resolved": "https://npm-proxy.dev.databricks.com/from2/-/from2-2.3.0.tgz", - "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", "license": "MIT", "dependencies": { "inherits": "^2.0.1", @@ -5583,9 +4886,6 @@ }, "node_modules/fsevents": { "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -5597,8 +4897,6 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://npm-proxy.dev.databricks.com/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5606,8 +4904,6 @@ }, "node_modules/gensync": { "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -5615,14 +4911,10 @@ }, "node_modules/geojson-vt": { "version": "3.2.1", - "resolved": "https://npm-proxy.dev.databricks.com/geojson-vt/-/geojson-vt-3.2.1.tgz", - "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==", "license": "ISC" }, "node_modules/get-caller-file": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, "license": "ISC", "engines": { @@ -5631,14 +4923,10 @@ }, "node_modules/get-canvas-context": { "version": "1.0.2", - "resolved": "https://npm-proxy.dev.databricks.com/get-canvas-context/-/get-canvas-context-1.0.2.tgz", - "integrity": "sha512-LnpfLf/TNzr9zVOGiIY6aKCz8EKuXmlYNV7CM2pUjBa/B+c2I15tS7KLySep75+FuerJdmArvJLcsAXWEy2H0A==", "license": "MIT" }, "node_modules/get-nonce": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", "license": "MIT", "engines": { "node": ">=6" @@ -5646,8 +4934,6 @@ }, "node_modules/get-stream": { "version": "6.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "license": "MIT", "engines": { "node": ">=10" @@ -5658,8 +4944,6 @@ }, "node_modules/get-tsconfig": { "version": "4.13.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", - "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -5670,20 +4954,14 @@ }, "node_modules/gl-mat4": { "version": "1.2.0", - "resolved": "https://npm-proxy.dev.databricks.com/gl-mat4/-/gl-mat4-1.2.0.tgz", - "integrity": "sha512-sT5C0pwB1/e9G9AvAoLsoaJtbMGjfd/jfxo8jMCKqYYEnjZuFvqV5rehqar0538EmssjdDeiEWnKyBSTw7quoA==", "license": "Zlib" }, "node_modules/gl-matrix": { "version": "3.4.4", - "resolved": "https://npm-proxy.dev.databricks.com/gl-matrix/-/gl-matrix-3.4.4.tgz", - "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", "license": "MIT" }, "node_modules/gl-text": { "version": "1.4.0", - "resolved": "https://npm-proxy.dev.databricks.com/gl-text/-/gl-text-1.4.0.tgz", - "integrity": "sha512-o47+XBqLCj1efmuNyCHt7/UEJmB9l66ql7pnobD6p+sgmBUdzfMZXIF0zD2+KRfpd99DJN+QXdvTFAGCKCVSmQ==", "license": "MIT", "dependencies": { "bit-twiddle": "^1.0.2", @@ -5707,8 +4985,6 @@ }, "node_modules/gl-util": { "version": "3.1.3", - "resolved": "https://npm-proxy.dev.databricks.com/gl-util/-/gl-util-3.1.3.tgz", - "integrity": "sha512-dvRTggw5MSkJnCbh74jZzSoTOGnVYK+Bt+Ckqm39CVcl6+zSsxqWk4lr5NKhkqXHL6qvZAU9h17ZF8mIskY9mA==", "license": "MIT", "dependencies": { "is-browser": "^2.0.1", @@ -5722,8 +4998,6 @@ }, "node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { @@ -5735,8 +5009,6 @@ }, "node_modules/global-prefix": { "version": "4.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/global-prefix/-/global-prefix-4.0.0.tgz", - "integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==", "license": "MIT", "dependencies": { "ini": "^4.1.3", @@ -5749,8 +5021,6 @@ }, "node_modules/global-prefix/node_modules/isexe": { "version": "3.1.5", - "resolved": "https://npm-proxy.dev.databricks.com/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -5758,8 +5028,6 @@ }, "node_modules/global-prefix/node_modules/which": { "version": "4.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", "license": "ISC", "dependencies": { "isexe": "^3.1.1" @@ -5773,8 +5041,6 @@ }, "node_modules/globals": { "version": "16.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", - "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", "dev": true, "license": "MIT", "engines": { @@ -5786,8 +5052,6 @@ }, "node_modules/glsl-inject-defines": { "version": "1.0.3", - "resolved": "https://npm-proxy.dev.databricks.com/glsl-inject-defines/-/glsl-inject-defines-1.0.3.tgz", - "integrity": "sha512-W49jIhuDtF6w+7wCMcClk27a2hq8znvHtlGnrYkSWEr8tHe9eA2dcnohlcAmxLYBSpSSdzOkRdyPTrx9fw49+A==", "license": "MIT", "dependencies": { "glsl-token-inject-block": "^1.0.0", @@ -5797,8 +5061,6 @@ }, "node_modules/glsl-resolve": { "version": "0.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/glsl-resolve/-/glsl-resolve-0.0.1.tgz", - "integrity": "sha512-xxFNsfnhZTK9NBhzJjSBGX6IOqYpvBHxxmo+4vapiljyGNCY0Bekzn0firQkQrazK59c1hYxMDxYS8MDlhw4gA==", "license": "MIT", "dependencies": { "resolve": "^0.6.1", @@ -5807,28 +5069,20 @@ }, "node_modules/glsl-resolve/node_modules/resolve": { "version": "0.6.3", - "resolved": "https://npm-proxy.dev.databricks.com/resolve/-/resolve-0.6.3.tgz", - "integrity": "sha512-UHBY3viPlJKf85YijDUcikKX6tmF4SokIDp518ZDVT92JNDcG5uKIthaT/owt3Sar0lwtOafsQuwrg22/v2Dwg==", "license": "MIT" }, "node_modules/glsl-resolve/node_modules/xtend": { "version": "2.2.0", - "resolved": "https://npm-proxy.dev.databricks.com/xtend/-/xtend-2.2.0.tgz", - "integrity": "sha512-SLt5uylT+4aoXxXuwtQp5ZnMMzhDb1Xkg4pEqc00WUJCQifPfV9Ub1VrNhp9kXkrjZD2I2Hl8WnjP37jzZLPZw==", "engines": { "node": ">=0.4" } }, "node_modules/glsl-token-assignments": { "version": "2.0.2", - "resolved": "https://npm-proxy.dev.databricks.com/glsl-token-assignments/-/glsl-token-assignments-2.0.2.tgz", - "integrity": "sha512-OwXrxixCyHzzA0U2g4btSNAyB2Dx8XrztY5aVUCjRSh4/D0WoJn8Qdps7Xub3sz6zE73W3szLrmWtQ7QMpeHEQ==", "license": "MIT" }, "node_modules/glsl-token-defines": { "version": "1.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/glsl-token-defines/-/glsl-token-defines-1.0.0.tgz", - "integrity": "sha512-Vb5QMVeLjmOwvvOJuPNg3vnRlffscq2/qvIuTpMzuO/7s5kT+63iL6Dfo2FYLWbzuiycWpbC0/KV0biqFwHxaQ==", "license": "MIT", "dependencies": { "glsl-tokenizer": "^2.0.0" @@ -5836,14 +5090,10 @@ }, "node_modules/glsl-token-depth": { "version": "1.1.2", - "resolved": "https://npm-proxy.dev.databricks.com/glsl-token-depth/-/glsl-token-depth-1.1.2.tgz", - "integrity": "sha512-eQnIBLc7vFf8axF9aoi/xW37LSWd2hCQr/3sZui8aBJnksq9C7zMeUYHVJWMhFzXrBU7fgIqni4EhXVW4/krpg==", "license": "MIT" }, "node_modules/glsl-token-descope": { "version": "1.0.2", - "resolved": "https://npm-proxy.dev.databricks.com/glsl-token-descope/-/glsl-token-descope-1.0.2.tgz", - "integrity": "sha512-kS2PTWkvi/YOeicVjXGgX5j7+8N7e56srNDEHDTVZ1dcESmbmpmgrnpjPcjxJjMxh56mSXYoFdZqb90gXkGjQw==", "license": "MIT", "dependencies": { "glsl-token-assignments": "^2.0.0", @@ -5854,38 +5104,26 @@ }, "node_modules/glsl-token-inject-block": { "version": "1.1.0", - "resolved": "https://npm-proxy.dev.databricks.com/glsl-token-inject-block/-/glsl-token-inject-block-1.1.0.tgz", - "integrity": "sha512-q/m+ukdUBuHCOtLhSr0uFb/qYQr4/oKrPSdIK2C4TD+qLaJvqM9wfXIF/OOBjuSA3pUoYHurVRNao6LTVVUPWA==", "license": "MIT" }, "node_modules/glsl-token-properties": { "version": "1.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/glsl-token-properties/-/glsl-token-properties-1.0.1.tgz", - "integrity": "sha512-dSeW1cOIzbuUoYH0y+nxzwK9S9O3wsjttkq5ij9ZGw0OS41BirKJzzH48VLm8qLg+au6b0sINxGC0IrGwtQUcA==", "license": "MIT" }, "node_modules/glsl-token-scope": { "version": "1.1.2", - "resolved": "https://npm-proxy.dev.databricks.com/glsl-token-scope/-/glsl-token-scope-1.1.2.tgz", - "integrity": "sha512-YKyOMk1B/tz9BwYUdfDoHvMIYTGtVv2vbDSLh94PT4+f87z21FVdou1KNKgF+nECBTo0fJ20dpm0B1vZB1Q03A==", "license": "MIT" }, "node_modules/glsl-token-string": { "version": "1.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/glsl-token-string/-/glsl-token-string-1.0.1.tgz", - "integrity": "sha512-1mtQ47Uxd47wrovl+T6RshKGkRRCYWhnELmkEcUAPALWGTFe2XZpH3r45XAwL2B6v+l0KNsCnoaZCSnhzKEksg==", "license": "MIT" }, "node_modules/glsl-token-whitespace-trim": { "version": "1.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/glsl-token-whitespace-trim/-/glsl-token-whitespace-trim-1.0.0.tgz", - "integrity": "sha512-ZJtsPut/aDaUdLUNtmBYhaCmhIjpKNg7IgZSfX5wFReMc2vnj8zok+gB/3Quqs0TsBSX/fGnqUUYZDqyuc2xLQ==", "license": "MIT" }, "node_modules/glsl-tokenizer": { "version": "2.1.5", - "resolved": "https://npm-proxy.dev.databricks.com/glsl-tokenizer/-/glsl-tokenizer-2.1.5.tgz", - "integrity": "sha512-XSZEJ/i4dmz3Pmbnpsy3cKh7cotvFlBiZnDOwnj/05EwNp2XrhQ4XKJxT7/pDt4kp4YcpRSKz8eTV7S+mwV6MA==", "license": "MIT", "dependencies": { "through2": "^0.6.3" @@ -5893,14 +5131,10 @@ }, "node_modules/glsl-tokenizer/node_modules/isarray": { "version": "0.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", "license": "MIT" }, "node_modules/glsl-tokenizer/node_modules/readable-stream": { "version": "1.0.34", - "resolved": "https://npm-proxy.dev.databricks.com/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -5911,14 +5145,10 @@ }, "node_modules/glsl-tokenizer/node_modules/string_decoder": { "version": "0.10.31", - "resolved": "https://npm-proxy.dev.databricks.com/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", "license": "MIT" }, "node_modules/glsl-tokenizer/node_modules/through2": { "version": "0.6.5", - "resolved": "https://npm-proxy.dev.databricks.com/through2/-/through2-0.6.5.tgz", - "integrity": "sha512-RkK/CCESdTKQZHdmKICijdKKsCRVHs5KsLZ6pACAmF/1GPUQhonHSXWNERctxEp7RmvjdNbZTL5z9V7nSCXKcg==", "license": "MIT", "dependencies": { "readable-stream": ">=1.0.33-1 <1.1.0-0", @@ -5927,8 +5157,6 @@ }, "node_modules/glslify": { "version": "7.1.1", - "resolved": "https://npm-proxy.dev.databricks.com/glslify/-/glslify-7.1.1.tgz", - "integrity": "sha512-bud98CJ6kGZcP9Yxcsi7Iz647wuDz3oN+IZsjCRi5X1PI7t/xPKeL0mOwXJjo+CRZMqvq0CkSJiywCcY7kVYog==", "license": "MIT", "dependencies": { "bl": "^2.2.1", @@ -5953,8 +5181,6 @@ }, "node_modules/glslify-bundle": { "version": "5.1.1", - "resolved": "https://npm-proxy.dev.databricks.com/glslify-bundle/-/glslify-bundle-5.1.1.tgz", - "integrity": "sha512-plaAOQPv62M1r3OsWf2UbjN0hUYAB7Aph5bfH58VxJZJhloRNbxOL9tl/7H71K7OLJoSJ2ZqWOKk3ttQ6wy24A==", "license": "MIT", "dependencies": { "glsl-inject-defines": "^1.0.1", @@ -5971,8 +5197,6 @@ }, "node_modules/glslify-deps": { "version": "1.3.2", - "resolved": "https://npm-proxy.dev.databricks.com/glslify-deps/-/glslify-deps-1.3.2.tgz", - "integrity": "sha512-7S7IkHWygJRjcawveXQjRXLO2FTjijPDYC7QfZyAQanY+yGLCFHYnPtsGT9bdyHiwPTw/5a1m1M9hamT2aBpag==", "license": "ISC", "dependencies": { "@choojs/findup": "^0.2.0", @@ -5987,8 +5211,6 @@ }, "node_modules/goober": { "version": "2.1.18", - "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", - "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==", "license": "MIT", "peerDependencies": { "csstype": "^3.0.10" @@ -5996,27 +5218,19 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, "node_modules/graphemer": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true, "license": "MIT" }, "node_modules/grid-index": { "version": "1.1.0", - "resolved": "https://npm-proxy.dev.databricks.com/grid-index/-/grid-index-1.1.0.tgz", - "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==", "license": "ISC" }, "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { @@ -6025,8 +5239,6 @@ }, "node_modules/has-hover": { "version": "1.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/has-hover/-/has-hover-1.0.1.tgz", - "integrity": "sha512-0G6w7LnlcpyDzpeGUTuT0CEw05+QlMuGVk1IHNAlHrGJITGodjZu3x8BNDUMfKJSZXNB2ZAclqc1bvrd+uUpfg==", "license": "MIT", "dependencies": { "is-browser": "^2.0.1" @@ -6034,8 +5246,6 @@ }, "node_modules/has-passive-events": { "version": "1.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/has-passive-events/-/has-passive-events-1.0.0.tgz", - "integrity": "sha512-2vSj6IeIsgvsRMyeQ0JaCX5Q3lX4zMn5HpoVc7MEhQ6pv8Iq9rsXjsp+E5ZwaT7T0xhMT0KmU8gtt1EFVdbJiw==", "license": "MIT", "dependencies": { "is-browser": "^2.0.1" @@ -6043,8 +5253,6 @@ }, "node_modules/hasown": { "version": "2.0.3", - "resolved": "https://npm-proxy.dev.databricks.com/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -6055,8 +5263,6 @@ }, "node_modules/hast-util-to-html": { "version": "9.0.5", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", - "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", "dev": true, "license": "MIT", "dependencies": { @@ -6079,8 +5285,6 @@ }, "node_modules/hast-util-whitespace": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", "dev": true, "license": "MIT", "dependencies": { @@ -6093,8 +5297,6 @@ }, "node_modules/html-void-elements": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", - "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", "dev": true, "license": "MIT", "funding": { @@ -6104,8 +5306,6 @@ }, "node_modules/iconv-lite": { "version": "0.4.24", - "resolved": "https://npm-proxy.dev.databricks.com/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -6116,8 +5316,6 @@ }, "node_modules/ieee754": { "version": "1.2.1", - "resolved": "https://npm-proxy.dev.databricks.com/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "funding": [ { "type": "github", @@ -6136,8 +5334,6 @@ }, "node_modules/ignore": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -6146,8 +5342,6 @@ }, "node_modules/immer": { "version": "10.2.0", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", - "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", "license": "MIT", "funding": { "type": "opencollective", @@ -6156,8 +5350,6 @@ }, "node_modules/import-fresh": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6173,8 +5365,6 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { @@ -6183,14 +5373,10 @@ }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://npm-proxy.dev.databricks.com/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, "node_modules/ini": { "version": "4.1.3", - "resolved": "https://npm-proxy.dev.databricks.com/ini/-/ini-4.1.3.tgz", - "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -6198,8 +5384,6 @@ }, "node_modules/internmap": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", "license": "ISC", "engines": { "node": ">=12" @@ -6207,8 +5391,6 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -6219,14 +5401,10 @@ }, "node_modules/is-browser": { "version": "2.1.0", - "resolved": "https://npm-proxy.dev.databricks.com/is-browser/-/is-browser-2.1.0.tgz", - "integrity": "sha512-F5rTJxDQ2sW81fcfOR1GnCXT6sVJC104fCyfj+mjpwNEwaPYSn5fte5jiHmBg3DHsIoL/l8Kvw5VN5SsTRcRFQ==", "license": "MIT" }, "node_modules/is-core-module": { "version": "2.16.1", - "resolved": "https://npm-proxy.dev.databricks.com/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -6240,8 +5418,6 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6249,8 +5425,6 @@ }, "node_modules/is-finite": { "version": "1.1.0", - "resolved": "https://npm-proxy.dev.databricks.com/is-finite/-/is-finite-1.1.0.tgz", - "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6261,8 +5435,6 @@ }, "node_modules/is-firefox": { "version": "1.0.3", - "resolved": "https://npm-proxy.dev.databricks.com/is-firefox/-/is-firefox-1.0.3.tgz", - "integrity": "sha512-6Q9ITjvWIm0Xdqv+5U12wgOKEM2KoBw4Y926m0OFkvlCxnbG94HKAsVz8w3fWcfAS5YA2fJORXX1dLrkprCCxA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6270,8 +5442,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", "engines": { @@ -6280,8 +5450,6 @@ }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -6292,14 +5460,10 @@ }, "node_modules/is-mobile": { "version": "4.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/is-mobile/-/is-mobile-4.0.0.tgz", - "integrity": "sha512-mlcHZA84t1qLSuWkt2v0I2l61PYdyQDt4aG1mLIXF5FDMm4+haBCxCPYSr/uwqQNRk1MiTizn0ypEuRAOLRAew==", "license": "MIT" }, "node_modules/is-number": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "license": "MIT", "engines": { "node": ">=0.12.0" @@ -6307,8 +5471,6 @@ }, "node_modules/is-obj": { "version": "1.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6316,8 +5478,6 @@ }, "node_modules/is-plain-obj": { "version": "1.1.0", - "resolved": "https://npm-proxy.dev.databricks.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6325,26 +5485,18 @@ }, "node_modules/is-string-blank": { "version": "1.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/is-string-blank/-/is-string-blank-1.0.1.tgz", - "integrity": "sha512-9H+ZBCVs3L9OYqv8nuUAzpcT9OTgMD1yAWrG7ihlnibdkbtB850heAmYWxHuXc4CHy4lKeK69tN+ny1K7gBIrw==", "license": "MIT" }, "node_modules/is-svg-path": { "version": "1.0.2", - "resolved": "https://npm-proxy.dev.databricks.com/is-svg-path/-/is-svg-path-1.0.2.tgz", - "integrity": "sha512-Lj4vePmqpPR1ZnRctHv8ltSh1OrSxHkhUkd7wi+VQdcdP15/KvQFyk7LhNuM7ZW0EVbJz8kZLVmL9quLrfq4Kg==", "license": "MIT" }, "node_modules/isarray": { "version": "2.0.5", - "resolved": "https://npm-proxy.dev.databricks.com/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "license": "MIT" }, "node_modules/isbot": { "version": "5.1.31", - "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.31.tgz", - "integrity": "sha512-DPgQshehErHAqSCKDb3rNW03pa2wS/v5evvUqtxt6TTnHRqAG8FdzcSSJs9656pK6Y+NT7K9R4acEYXLHYfpUQ==", "license": "Unlicense", "engines": { "node": ">=18" @@ -6352,15 +5504,11 @@ }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "license": "ISC" }, "node_modules/jiti": { "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "devOptional": true, "license": "MIT", "bin": { @@ -6369,14 +5517,10 @@ }, "node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "license": "MIT", "dependencies": { @@ -6388,8 +5532,6 @@ }, "node_modules/jsesc": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -6400,35 +5542,25 @@ }, "node_modules/json-buffer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, "node_modules/json-stringify-pretty-compact": { "version": "4.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", - "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -6439,14 +5571,10 @@ }, "node_modules/kdbush": { "version": "4.0.2", - "resolved": "https://npm-proxy.dev.databricks.com/kdbush/-/kdbush-4.0.2.tgz", - "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", "license": "ISC" }, "node_modules/keyv": { "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { @@ -6455,8 +5583,6 @@ }, "node_modules/kind-of": { "version": "6.0.3", - "resolved": "https://npm-proxy.dev.databricks.com/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6464,8 +5590,6 @@ }, "node_modules/levn": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6478,8 +5602,6 @@ }, "node_modules/lightningcss": { "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -6527,8 +5649,6 @@ }, "node_modules/lightningcss-darwin-arm64": { "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", "cpu": [ "arm64" ], @@ -6727,8 +5847,6 @@ }, "node_modules/locate-path": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { @@ -6743,14 +5861,10 @@ }, "node_modules/lodash.merge": { "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "license": "MIT" }, "node_modules/loose-envify": { "version": "1.4.0", - "resolved": "https://npm-proxy.dev.databricks.com/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -6761,8 +5875,6 @@ }, "node_modules/lru-cache": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -6770,8 +5882,6 @@ }, "node_modules/lucide-react": { "version": "0.546.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.546.0.tgz", - "integrity": "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -6779,8 +5889,6 @@ }, "node_modules/magic-string": { "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6789,8 +5897,6 @@ }, "node_modules/map-limit": { "version": "0.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/map-limit/-/map-limit-0.0.1.tgz", - "integrity": "sha512-pJpcfLPnIF/Sk3taPW21G/RQsEEirGaFpCW3oXRwH9dnFHPHNGjNyvh++rdmC2fNqEaTw2MhYJraoJWAHx8kEg==", "license": "MIT", "dependencies": { "once": "~1.3.0" @@ -6798,8 +5904,6 @@ }, "node_modules/map-limit/node_modules/once": { "version": "1.3.3", - "resolved": "https://npm-proxy.dev.databricks.com/once/-/once-1.3.3.tgz", - "integrity": "sha512-6vaNInhu+CHxtONf3zw3vq4SP2DOQhjBvIa3rNcG0+P7eKWlYH6Peu7rHizSloRU2EwMz6GraLieis9Ac9+p1w==", "license": "ISC", "dependencies": { "wrappy": "1" @@ -6807,8 +5911,6 @@ }, "node_modules/mapbox-gl": { "version": "1.13.3", - "resolved": "https://npm-proxy.dev.databricks.com/mapbox-gl/-/mapbox-gl-1.13.3.tgz", - "integrity": "sha512-p8lJFEiqmEQlyv+DQxFAOG/XPWN0Wp7j/Psq93Zywz7qt9CcUKFYDBOoOEKzqe6gudHVJY8/Bhqw6VDpX2lSBg==", "license": "SEE LICENSE IN LICENSE.txt", "peer": true, "dependencies": { @@ -6841,8 +5943,6 @@ }, "node_modules/maplibre-gl": { "version": "4.7.1", - "resolved": "https://npm-proxy.dev.databricks.com/maplibre-gl/-/maplibre-gl-4.7.1.tgz", - "integrity": "sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==", "license": "BSD-3-Clause", "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", @@ -6882,44 +5982,30 @@ }, "node_modules/maplibre-gl/node_modules/@mapbox/tiny-sdf": { "version": "2.1.0", - "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/tiny-sdf/-/tiny-sdf-2.1.0.tgz", - "integrity": "sha512-uFJhNh36BR4OCuWIEiWaEix9CA2WzT6CAIcqVjWYpnx8+QDtS+oC4QehRrx5cX4mgWs37MmKnwUejeHxVymzNg==", "license": "BSD-2-Clause" }, "node_modules/maplibre-gl/node_modules/@mapbox/unitbezier": { "version": "0.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", - "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", "license": "BSD-2-Clause" }, "node_modules/maplibre-gl/node_modules/earcut": { "version": "3.0.2", - "resolved": "https://npm-proxy.dev.databricks.com/earcut/-/earcut-3.0.2.tgz", - "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", "license": "ISC" }, "node_modules/maplibre-gl/node_modules/geojson-vt": { "version": "4.0.2", - "resolved": "https://npm-proxy.dev.databricks.com/geojson-vt/-/geojson-vt-4.0.2.tgz", - "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", "license": "ISC" }, "node_modules/maplibre-gl/node_modules/potpack": { "version": "2.1.0", - "resolved": "https://npm-proxy.dev.databricks.com/potpack/-/potpack-2.1.0.tgz", - "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", "license": "ISC" }, "node_modules/maplibre-gl/node_modules/quickselect": { "version": "3.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/quickselect/-/quickselect-3.0.0.tgz", - "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", "license": "ISC" }, "node_modules/maplibre-gl/node_modules/supercluster": { "version": "8.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/supercluster/-/supercluster-8.0.1.tgz", - "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", "license": "ISC", "dependencies": { "kdbush": "^4.0.2" @@ -6927,14 +6013,10 @@ }, "node_modules/maplibre-gl/node_modules/tinyqueue": { "version": "3.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/tinyqueue/-/tinyqueue-3.0.0.tgz", - "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", "license": "ISC" }, "node_modules/math-log2": { "version": "1.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/math-log2/-/math-log2-1.0.1.tgz", - "integrity": "sha512-9W0yGtkaMAkf74XGYVy4Dqw3YUMnTNB2eeiw9aQbUl4A3KmuCEHTt2DgAB07ENzOYAjsYSAYufkAq0Zd+jU7zA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6942,8 +6024,6 @@ }, "node_modules/mdast-util-to-hast": { "version": "13.2.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", - "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "dev": true, "license": "MIT", "dependencies": { @@ -6964,8 +6044,6 @@ }, "node_modules/merge2": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", "engines": { @@ -6974,8 +6052,6 @@ }, "node_modules/micromark-util-character": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "dev": true, "funding": [ { @@ -6995,8 +6071,6 @@ }, "node_modules/micromark-util-encode": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", "dev": true, "funding": [ { @@ -7012,8 +6086,6 @@ }, "node_modules/micromark-util-sanitize-uri": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", "dev": true, "funding": [ { @@ -7034,8 +6106,6 @@ }, "node_modules/micromark-util-symbol": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "dev": true, "funding": [ { @@ -7051,8 +6121,6 @@ }, "node_modules/micromark-util-types": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", "dev": true, "funding": [ { @@ -7068,8 +6136,6 @@ }, "node_modules/micromatch": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { @@ -7082,8 +6148,6 @@ }, "node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -7095,8 +6159,6 @@ }, "node_modules/minimist": { "version": "1.2.8", - "resolved": "https://npm-proxy.dev.databricks.com/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7104,8 +6166,6 @@ }, "node_modules/mouse-change": { "version": "1.4.0", - "resolved": "https://npm-proxy.dev.databricks.com/mouse-change/-/mouse-change-1.4.0.tgz", - "integrity": "sha512-vpN0s+zLL2ykyyUDh+fayu9Xkor5v/zRD9jhSqjRS1cJTGS0+oakVZzNm5n19JvvEj0you+MXlYTpNxUDQUjkQ==", "license": "MIT", "dependencies": { "mouse-event": "^1.0.0" @@ -7113,20 +6173,14 @@ }, "node_modules/mouse-event": { "version": "1.0.5", - "resolved": "https://npm-proxy.dev.databricks.com/mouse-event/-/mouse-event-1.0.5.tgz", - "integrity": "sha512-ItUxtL2IkeSKSp9cyaX2JLUuKk2uMoxBg4bbOWVd29+CskYJR9BGsUqtXenNzKbnDshvupjUewDIYVrOB6NmGw==", "license": "MIT" }, "node_modules/mouse-event-offset": { "version": "3.0.2", - "resolved": "https://npm-proxy.dev.databricks.com/mouse-event-offset/-/mouse-event-offset-3.0.2.tgz", - "integrity": "sha512-s9sqOs5B1Ykox3Xo8b3Ss2IQju4UwlW6LSR+Q5FXWpprJ5fzMLefIIItr3PH8RwzfGy6gxs/4GAmiNuZScE25w==", "license": "MIT" }, "node_modules/mouse-wheel": { "version": "1.2.0", - "resolved": "https://npm-proxy.dev.databricks.com/mouse-wheel/-/mouse-wheel-1.2.0.tgz", - "integrity": "sha512-+OfYBiUOCTWcTECES49neZwL5AoGkXE+lFjIvzwNCnYRlso+EnfvovcBxGoyQ0yQt806eSPjS675K0EwWknXmw==", "license": "MIT", "dependencies": { "right-now": "^1.0.0", @@ -7136,20 +6190,14 @@ }, "node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/murmurhash-js": { "version": "1.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/murmurhash-js/-/murmurhash-js-1.0.0.tgz", - "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -7166,21 +6214,15 @@ }, "node_modules/native-promise-only": { "version": "0.8.1", - "resolved": "https://npm-proxy.dev.databricks.com/native-promise-only/-/native-promise-only-0.8.1.tgz", - "integrity": "sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==", "license": "MIT" }, "node_modules/natural-compare": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, "node_modules/needle": { "version": "2.9.1", - "resolved": "https://npm-proxy.dev.databricks.com/needle/-/needle-2.9.1.tgz", - "integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==", "license": "MIT", "dependencies": { "debug": "^3.2.6", @@ -7196,8 +6238,6 @@ }, "node_modules/needle/node_modules/debug": { "version": "3.2.7", - "resolved": "https://npm-proxy.dev.databricks.com/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "license": "MIT", "dependencies": { "ms": "^2.1.1" @@ -7205,20 +6245,14 @@ }, "node_modules/next-tick": { "version": "1.1.0", - "resolved": "https://npm-proxy.dev.databricks.com/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", "license": "ISC" }, "node_modules/node-releases": { "version": "2.0.23", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", - "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7226,8 +6260,6 @@ }, "node_modules/normalize-range": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true, "license": "MIT", "engines": { @@ -7236,14 +6268,10 @@ }, "node_modules/normalize-svg-path": { "version": "0.1.0", - "resolved": "https://npm-proxy.dev.databricks.com/normalize-svg-path/-/normalize-svg-path-0.1.0.tgz", - "integrity": "sha512-1/kmYej2iedi5+ROxkRESL/pI02pkg0OBnaR4hJkSIX6+ORzepwbuUXfrdZaPjysTsJInj0Rj5NuX027+dMBvA==", "license": "MIT" }, "node_modules/number-is-integer": { "version": "1.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/number-is-integer/-/number-is-integer-1.0.1.tgz", - "integrity": "sha512-Dq3iuiFBkrbmuQjGFFF3zckXNCQoSD37/SdSbgcBailUx6knDvDwb5CympBgcoWHy36sfS12u74MHYkXyHq6bg==", "license": "MIT", "dependencies": { "is-finite": "^1.0.1" @@ -7254,8 +6282,6 @@ }, "node_modules/object-assign": { "version": "4.1.1", - "resolved": "https://npm-proxy.dev.databricks.com/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7263,8 +6289,6 @@ }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://npm-proxy.dev.databricks.com/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "license": "ISC", "dependencies": { "wrappy": "1" @@ -7272,15 +6296,11 @@ }, "node_modules/oniguruma-parser": { "version": "0.12.1", - "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", - "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", "dev": true, "license": "MIT" }, "node_modules/oniguruma-to-es": { "version": "4.3.4", - "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz", - "integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==", "dev": true, "license": "MIT", "dependencies": { @@ -7291,8 +6311,6 @@ }, "node_modules/optionator": { "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { @@ -7309,8 +6327,6 @@ }, "node_modules/p-limit": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7325,8 +6341,6 @@ }, "node_modules/p-locate": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -7341,8 +6355,6 @@ }, "node_modules/parent-module": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { @@ -7354,14 +6366,10 @@ }, "node_modules/parenthesis": { "version": "3.1.8", - "resolved": "https://npm-proxy.dev.databricks.com/parenthesis/-/parenthesis-3.1.8.tgz", - "integrity": "sha512-KF/U8tk54BgQewkJPvB4s/US3VQY68BRDpH638+7O/n58TpnwiwnOtGIOsT2/i+M78s61BBpeC83STB88d8sqw==", "license": "MIT" }, "node_modules/parse-rect": { "version": "1.2.0", - "resolved": "https://npm-proxy.dev.databricks.com/parse-rect/-/parse-rect-1.2.0.tgz", - "integrity": "sha512-4QZ6KYbnE6RTwg9E0HpLchUM9EZt6DnDxajFZZDSV4p/12ZJEvPO702DZpGvRYEPo00yKDys7jASi+/w7aO8LA==", "license": "MIT", "dependencies": { "pick-by-alias": "^1.2.0" @@ -7369,20 +6377,14 @@ }, "node_modules/parse-svg-path": { "version": "0.1.2", - "resolved": "https://npm-proxy.dev.databricks.com/parse-svg-path/-/parse-svg-path-0.1.2.tgz", - "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", "license": "MIT" }, "node_modules/parse-unit": { "version": "1.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/parse-unit/-/parse-unit-1.0.1.tgz", - "integrity": "sha512-hrqldJHokR3Qj88EIlV/kAyAi/G5R2+R56TBANxNMy0uPlYcttx0jnMW6Yx5KsKPSbC3KddM/7qQm3+0wEXKxg==", "license": "MIT" }, "node_modules/path-exists": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", "engines": { @@ -7391,8 +6393,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { @@ -7401,20 +6401,14 @@ }, "node_modules/path-parse": { "version": "1.0.7", - "resolved": "https://npm-proxy.dev.databricks.com/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, "node_modules/pathe": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, "node_modules/pbf": { "version": "3.3.0", - "resolved": "https://npm-proxy.dev.databricks.com/pbf/-/pbf-3.3.0.tgz", - "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", "license": "BSD-3-Clause", "dependencies": { "ieee754": "^1.1.12", @@ -7426,26 +6420,18 @@ }, "node_modules/performance-now": { "version": "2.1.0", - "resolved": "https://npm-proxy.dev.databricks.com/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "license": "MIT" }, "node_modules/pick-by-alias": { "version": "1.2.0", - "resolved": "https://npm-proxy.dev.databricks.com/pick-by-alias/-/pick-by-alias-1.2.0.tgz", - "integrity": "sha512-ESj2+eBxhGrcA1azgHs7lARG5+5iLakc/6nlfbpjcLl00HuuUOIuORhYXN4D1HfvMSKuVtFQjAlnwi1JHEeDIw==", "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -7456,8 +6442,6 @@ }, "node_modules/plotly.js": { "version": "3.5.0", - "resolved": "https://npm-proxy.dev.databricks.com/plotly.js/-/plotly.js-3.5.0.tgz", - "integrity": "sha512-a3AYQIMG7OdZmrJ/fJ65HSt3g1l5qDeludKqjjafU1dh5E+fwqDhsEBndW7VCYwjlducCfN6KtPdWdiWFcoBWw==", "license": "MIT", "dependencies": { "@plotly/d3": "3.8.2", @@ -7517,20 +6501,14 @@ }, "node_modules/plotly.js/node_modules/d3-format": { "version": "1.4.5", - "resolved": "https://npm-proxy.dev.databricks.com/d3-format/-/d3-format-1.4.5.tgz", - "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==", "license": "BSD-3-Clause" }, "node_modules/plotly.js/node_modules/d3-time": { "version": "1.1.0", - "resolved": "https://npm-proxy.dev.databricks.com/d3-time/-/d3-time-1.1.0.tgz", - "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==", "license": "BSD-3-Clause" }, "node_modules/plotly.js/node_modules/d3-time-format": { "version": "2.3.0", - "resolved": "https://npm-proxy.dev.databricks.com/d3-time-format/-/d3-time-format-2.3.0.tgz", - "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", "license": "BSD-3-Clause", "dependencies": { "d3-time": "1" @@ -7538,20 +6516,14 @@ }, "node_modules/point-in-polygon": { "version": "1.1.0", - "resolved": "https://npm-proxy.dev.databricks.com/point-in-polygon/-/point-in-polygon-1.1.0.tgz", - "integrity": "sha512-3ojrFwjnnw8Q9242TzgXuTD+eKiutbzyslcq1ydfu82Db2y+Ogbmyrkpv0Hgj31qwT3lbS9+QAAO/pIQM35XRw==", "license": "MIT" }, "node_modules/polybooljs": { "version": "1.2.2", - "resolved": "https://npm-proxy.dev.databricks.com/polybooljs/-/polybooljs-1.2.2.tgz", - "integrity": "sha512-ziHW/02J0XuNuUtmidBc6GXE8YohYydp3DWPWXYsd7O721TjcmN+k6ezjdwkDqep+gnWnFY+yqZHvzElra2oCg==", "license": "MIT" }, "node_modules/postcss": { "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -7578,21 +6550,15 @@ }, "node_modules/postcss-value-parser": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true, "license": "MIT" }, "node_modules/potpack": { "version": "1.0.2", - "resolved": "https://npm-proxy.dev.databricks.com/potpack/-/potpack-1.0.2.tgz", - "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", "license": "ISC" }, "node_modules/prelude-ls": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { @@ -7601,8 +6567,6 @@ }, "node_modules/prettier": { "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -7616,8 +6580,6 @@ }, "node_modules/probe-image-size": { "version": "7.2.3", - "resolved": "https://npm-proxy.dev.databricks.com/probe-image-size/-/probe-image-size-7.2.3.tgz", - "integrity": "sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w==", "license": "MIT", "dependencies": { "lodash.merge": "^4.6.2", @@ -7627,14 +6589,10 @@ }, "node_modules/process-nextick-args": { "version": "2.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, "node_modules/prop-types": { "version": "15.8.1", - "resolved": "https://npm-proxy.dev.databricks.com/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -7644,14 +6602,10 @@ }, "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", - "resolved": "https://npm-proxy.dev.databricks.com/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, "node_modules/property-information": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", "dev": true, "license": "MIT", "funding": { @@ -7661,14 +6615,10 @@ }, "node_modules/protocol-buffers-schema": { "version": "3.6.1", - "resolved": "https://npm-proxy.dev.databricks.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz", - "integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==", "license": "MIT" }, "node_modules/punycode": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { @@ -7677,8 +6627,6 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { @@ -7698,14 +6646,10 @@ }, "node_modules/quickselect": { "version": "2.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/quickselect/-/quickselect-2.0.0.tgz", - "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", "license": "ISC" }, "node_modules/raf": { "version": "3.4.1", - "resolved": "https://npm-proxy.dev.databricks.com/raf/-/raf-3.4.1.tgz", - "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", "license": "MIT", "dependencies": { "performance-now": "^2.1.0" @@ -7713,8 +6657,6 @@ }, "node_modules/react": { "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", - "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7722,8 +6664,6 @@ }, "node_modules/react-dom": { "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", - "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" @@ -7734,15 +6674,11 @@ }, "node_modules/react-is": { "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT", "peer": true }, "node_modules/react-plotly.js": { "version": "2.6.0", - "resolved": "https://npm-proxy.dev.databricks.com/react-plotly.js/-/react-plotly.js-2.6.0.tgz", - "integrity": "sha512-g93xcyhAVCSt9kV1svqG1clAEdL6k3U+jjuSzfTV7owaSU9Go6Ph8bl25J+jKfKvIGAEYpe4qj++WHJuc9IaeA==", "license": "MIT", "dependencies": { "prop-types": "^15.8.1" @@ -7754,8 +6690,6 @@ }, "node_modules/react-redux": { "version": "9.2.0", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", - "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", "dependencies": { "@types/use-sync-external-store": "^0.0.6", @@ -7777,8 +6711,6 @@ }, "node_modules/react-refresh": { "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", "dev": true, "license": "MIT", "engines": { @@ -7787,8 +6719,6 @@ }, "node_modules/react-remove-scroll": { "version": "2.7.1", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", - "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", "license": "MIT", "dependencies": { "react-remove-scroll-bar": "^2.3.7", @@ -7812,8 +6742,6 @@ }, "node_modules/react-remove-scroll-bar": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", "license": "MIT", "dependencies": { "react-style-singleton": "^2.2.2", @@ -7834,8 +6762,6 @@ }, "node_modules/react-style-singleton": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", "license": "MIT", "dependencies": { "get-nonce": "^1.0.0", @@ -7856,8 +6782,6 @@ }, "node_modules/readable-stream": { "version": "2.3.8", - "resolved": "https://npm-proxy.dev.databricks.com/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -7871,20 +6795,14 @@ }, "node_modules/readable-stream/node_modules/isarray": { "version": "1.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, "node_modules/readable-stream/node_modules/safe-buffer": { "version": "5.1.2", - "resolved": "https://npm-proxy.dev.databricks.com/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, "node_modules/readdirp": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -7895,8 +6813,6 @@ }, "node_modules/recast": { "version": "0.23.11", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", - "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", "license": "MIT", "dependencies": { "ast-types": "^0.16.1", @@ -7911,8 +6827,6 @@ }, "node_modules/recast/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -7920,8 +6834,6 @@ }, "node_modules/recharts": { "version": "3.4.1", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.4.1.tgz", - "integrity": "sha512-35kYg6JoOgwq8sE4rhYkVWwa6aAIgOtT+Ob0gitnShjwUwZmhrmy7Jco/5kJNF4PnLXgt9Hwq+geEMS+WrjU1g==", "license": "MIT", "workspaces": [ "www" @@ -7950,14 +6862,10 @@ }, "node_modules/redux": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", - "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", "license": "MIT", "peerDependencies": { "redux": "^5.0.0" @@ -7965,8 +6873,6 @@ }, "node_modules/regex": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz", - "integrity": "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==", "dev": true, "license": "MIT", "dependencies": { @@ -7975,8 +6881,6 @@ }, "node_modules/regex-recursion": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", - "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", "dev": true, "license": "MIT", "dependencies": { @@ -7985,21 +6889,15 @@ }, "node_modules/regex-utilities": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", - "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", "dev": true, "license": "MIT" }, "node_modules/regl": { "version": "2.1.1", - "resolved": "https://npm-proxy.dev.databricks.com/regl/-/regl-2.1.1.tgz", - "integrity": "sha512-+IOGrxl3FZ8ZM9ixCWQZzFRiRn7Rzn9bu3iFHwg/yz4tlOUQgbO4PHLgG+1ZT60zcIV8tief6Qrmyl8qcoJP0g==", "license": "MIT" }, "node_modules/regl-error2d": { "version": "2.0.12", - "resolved": "https://npm-proxy.dev.databricks.com/regl-error2d/-/regl-error2d-2.0.12.tgz", - "integrity": "sha512-r7BUprZoPO9AbyqM5qlJesrSRkl+hZnVKWKsVp7YhOl/3RIpi4UDGASGJY0puQ96u5fBYw/OlqV24IGcgJ0McA==", "license": "MIT", "dependencies": { "array-bounds": "^1.0.1", @@ -8013,8 +6911,6 @@ }, "node_modules/regl-line2d": { "version": "3.1.3", - "resolved": "https://npm-proxy.dev.databricks.com/regl-line2d/-/regl-line2d-3.1.3.tgz", - "integrity": "sha512-fkgzW+tTn4QUQLpFKsUIE0sgWdCmXAM3ctXcCgoGBZTSX5FE2A0M7aynz7nrZT5baaftLrk9te54B+MEq4QcSA==", "license": "MIT", "dependencies": { "array-bounds": "^1.0.1", @@ -8032,8 +6928,6 @@ }, "node_modules/regl-scatter2d": { "version": "3.4.0", - "resolved": "https://npm-proxy.dev.databricks.com/regl-scatter2d/-/regl-scatter2d-3.4.0.tgz", - "integrity": "sha512-DavKQlHsI+iHZuLgOL+yGkg+sPd94CS+7FCBWkcQ6s/TbaNfUsF9eN591fjjSWIoKrGNfb/SEGhsXR5lXjqZ2w==", "license": "MIT", "dependencies": { "@plotly/point-cluster": "^3.1.9", @@ -8050,8 +6944,6 @@ }, "node_modules/regl-splom": { "version": "1.0.14", - "resolved": "https://npm-proxy.dev.databricks.com/regl-splom/-/regl-splom-1.0.14.tgz", - "integrity": "sha512-OiLqjmPRYbd7kDlHC6/zDf6L8lxgDC65BhC8JirhP4ykrK4x22ZyS+BnY8EUinXKDeMgmpRwCvUmk7BK4Nweuw==", "license": "MIT", "dependencies": { "array-bounds": "^1.0.1", @@ -8066,8 +6958,6 @@ }, "node_modules/require-directory": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, "license": "MIT", "engines": { @@ -8076,14 +6966,10 @@ }, "node_modules/reselect": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", - "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", "license": "MIT" }, "node_modules/resolve": { "version": "1.22.12", - "resolved": "https://npm-proxy.dev.databricks.com/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -8103,8 +6989,6 @@ }, "node_modules/resolve-from": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { @@ -8113,8 +6997,6 @@ }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" @@ -8122,8 +7004,6 @@ }, "node_modules/resolve-protobuf-schema": { "version": "2.1.0", - "resolved": "https://npm-proxy.dev.databricks.com/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", - "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", "license": "MIT", "dependencies": { "protocol-buffers-schema": "^3.3.1" @@ -8131,8 +7011,6 @@ }, "node_modules/reusify": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -8142,14 +7020,10 @@ }, "node_modules/right-now": { "version": "1.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/right-now/-/right-now-1.0.0.tgz", - "integrity": "sha512-DA8+YS+sMIVpbsuKgy+Z67L9Lxb1p05mNxRpDPNksPDEFir4vmBlUtuN9jkTGn9YMMdlBuK7XQgFiz6ws+yhSg==", "license": "MIT" }, "node_modules/rolldown": { "version": "1.0.0-beta.41", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.41.tgz", - "integrity": "sha512-U+NPR0Bkg3wm61dteD2L4nAM1U9dtaqVrpDXwC36IKRHpEO/Ubpid4Nijpa2imPchcVNHfxVFwSSMJdwdGFUbg==", "license": "MIT", "dependencies": { "@oxc-project/types": "=0.93.0", @@ -8181,14 +7055,10 @@ }, "node_modules/rolldown/node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.41", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.41.tgz", - "integrity": "sha512-ycMEPrS3StOIeb87BT3/+bu+blEtyvwQ4zmo2IcJQy0Rd1DAAhKksA0iUZ3MYSpJtjlPhg0Eo6mvVS6ggPhRbw==", "license": "MIT" }, "node_modules/run-parallel": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { @@ -8211,14 +7081,10 @@ }, "node_modules/rw": { "version": "1.3.3", - "resolved": "https://npm-proxy.dev.databricks.com/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", "license": "BSD-3-Clause" }, "node_modules/safe-buffer": { "version": "5.2.1", - "resolved": "https://npm-proxy.dev.databricks.com/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "funding": [ { "type": "github", @@ -8237,14 +7103,10 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "resolved": "https://npm-proxy.dev.databricks.com/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, "node_modules/sax": { "version": "1.6.0", - "resolved": "https://npm-proxy.dev.databricks.com/sax/-/sax-1.6.0.tgz", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -8252,14 +7114,10 @@ }, "node_modules/scheduler": { "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -8267,8 +7125,6 @@ }, "node_modules/seroval": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.1.tgz", - "integrity": "sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA==", "license": "MIT", "engines": { "node": ">=10" @@ -8276,8 +7132,6 @@ }, "node_modules/seroval-plugins": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.1.tgz", - "integrity": "sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw==", "license": "MIT", "engines": { "node": ">=10" @@ -8288,14 +7142,10 @@ }, "node_modules/shallow-copy": { "version": "0.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/shallow-copy/-/shallow-copy-0.0.1.tgz", - "integrity": "sha512-b6i4ZpVuUxB9h5gfCxPiusKYkqTMOjEbBs4wMaFbkfia4yFv92UKZ6Df8WXcKbn08JNL/abvg3FnMAOfakDvUw==", "license": "MIT" }, "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { @@ -8307,8 +7157,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { @@ -8317,8 +7165,6 @@ }, "node_modules/shiki": { "version": "3.15.0", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.15.0.tgz", - "integrity": "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw==", "dev": true, "license": "MIT", "dependencies": { @@ -8334,14 +7180,10 @@ }, "node_modules/signum": { "version": "1.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/signum/-/signum-1.0.0.tgz", - "integrity": "sha512-yodFGwcyt59XRh7w5W3jPcIQb3Bwi21suEfT7MAWnBX3iCdklJpgDgvGT9o04UonglZN5SNMfJFkHIR/jO8GHw==", "license": "MIT" }, "node_modules/solid-js": { "version": "1.9.11", - "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.11.tgz", - "integrity": "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q==", "license": "MIT", "peer": true, "dependencies": { @@ -8352,8 +7194,6 @@ }, "node_modules/source-map": { "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", "license": "BSD-3-Clause", "engines": { "node": ">= 12" @@ -8361,8 +7201,6 @@ }, "node_modules/source-map-js": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -8370,8 +7208,6 @@ }, "node_modules/space-separated-tokens": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", "dev": true, "license": "MIT", "funding": { @@ -8381,16 +7217,12 @@ }, "node_modules/stack-trace": { "version": "0.0.9", - "resolved": "https://npm-proxy.dev.databricks.com/stack-trace/-/stack-trace-0.0.9.tgz", - "integrity": "sha512-vjUc6sfgtgY0dxCdnc40mK6Oftjo9+2K8H/NG81TMhgL392FtiPA9tn9RLyTxXmTLPJPjF3VyzFp6bsWFLisMQ==", "engines": { "node": "*" } }, "node_modules/static-eval": { "version": "2.1.1", - "resolved": "https://npm-proxy.dev.databricks.com/static-eval/-/static-eval-2.1.1.tgz", - "integrity": "sha512-MgWpQ/ZjGieSVB3eOJVs4OA2LT/q1vx98KPCTTQPzq/aLr0YUXTsgryTXr4SLfR0ZfUUCiedM9n/ABeDIyy4mA==", "license": "MIT", "dependencies": { "escodegen": "^2.1.0" @@ -8398,8 +7230,6 @@ }, "node_modules/stream-parser": { "version": "0.3.1", - "resolved": "https://npm-proxy.dev.databricks.com/stream-parser/-/stream-parser-0.3.1.tgz", - "integrity": "sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==", "license": "MIT", "dependencies": { "debug": "2" @@ -8407,8 +7237,6 @@ }, "node_modules/stream-parser/node_modules/debug": { "version": "2.6.9", - "resolved": "https://npm-proxy.dev.databricks.com/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -8416,20 +7244,14 @@ }, "node_modules/stream-parser/node_modules/ms": { "version": "2.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, "node_modules/stream-shift": { "version": "1.0.3", - "resolved": "https://npm-proxy.dev.databricks.com/stream-shift/-/stream-shift-1.0.3.tgz", - "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", "license": "MIT" }, "node_modules/string_decoder": { "version": "1.1.1", - "resolved": "https://npm-proxy.dev.databricks.com/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -8437,14 +7259,10 @@ }, "node_modules/string_decoder/node_modules/safe-buffer": { "version": "5.1.2", - "resolved": "https://npm-proxy.dev.databricks.com/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, "node_modules/string-split-by": { "version": "1.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/string-split-by/-/string-split-by-1.0.0.tgz", - "integrity": "sha512-KaJKY+hfpzNyet/emP81PJA9hTVSfxNLS9SFTWxdCnnW1/zOOwiV248+EfoX7IQFcBaOp4G5YE6xTJMF+pLg6A==", "license": "MIT", "dependencies": { "parenthesis": "^3.1.5" @@ -8452,8 +7270,6 @@ }, "node_modules/stringify-entities": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", "dev": true, "license": "MIT", "dependencies": { @@ -8467,8 +7283,6 @@ }, "node_modules/strip-json-comments": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", "engines": { @@ -8480,14 +7294,10 @@ }, "node_modules/strongly-connected-components": { "version": "1.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/strongly-connected-components/-/strongly-connected-components-1.0.1.tgz", - "integrity": "sha512-i0TFx4wPcO0FwX+4RkLJi1MxmcTv90jNZgxMu9XRnMXMeFUY1VJlIoXpZunPUvUUqbCT1pg5PEkFqqpcaElNaA==", "license": "MIT" }, "node_modules/supercluster": { "version": "7.1.5", - "resolved": "https://npm-proxy.dev.databricks.com/supercluster/-/supercluster-7.1.5.tgz", - "integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==", "license": "ISC", "dependencies": { "kdbush": "^3.0.0" @@ -8495,20 +7305,14 @@ }, "node_modules/supercluster/node_modules/kdbush": { "version": "3.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/kdbush/-/kdbush-3.0.0.tgz", - "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==", "license": "ISC" }, "node_modules/superscript-text": { "version": "1.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/superscript-text/-/superscript-text-1.0.0.tgz", - "integrity": "sha512-gwu8l5MtRZ6koO0icVTlmN5pm7Dhh1+Xpe9O4x6ObMAsW+3jPbW14d1DsBq1F4wiI+WOFjXF35pslgec/G8yCQ==", "license": "MIT" }, "node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -8520,8 +7324,6 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "resolved": "https://npm-proxy.dev.databricks.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -8532,14 +7334,10 @@ }, "node_modules/svg-arc-to-cubic-bezier": { "version": "3.2.0", - "resolved": "https://npm-proxy.dev.databricks.com/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", - "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==", "license": "ISC" }, "node_modules/svg-path-bounds": { "version": "1.0.2", - "resolved": "https://npm-proxy.dev.databricks.com/svg-path-bounds/-/svg-path-bounds-1.0.2.tgz", - "integrity": "sha512-H4/uAgLWrppIC0kHsb2/dWUYSmb4GE5UqH06uqWBcg6LBjX2fu0A8+JrO2/FJPZiSsNOKZAhyFFgsLTdYUvSqQ==", "license": "MIT", "dependencies": { "abs-svg-path": "^0.1.1", @@ -8550,8 +7348,6 @@ }, "node_modules/svg-path-bounds/node_modules/normalize-svg-path": { "version": "1.1.0", - "resolved": "https://npm-proxy.dev.databricks.com/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz", - "integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==", "license": "MIT", "dependencies": { "svg-arc-to-cubic-bezier": "^3.0.0" @@ -8559,8 +7355,6 @@ }, "node_modules/svg-path-sdf": { "version": "1.1.3", - "resolved": "https://npm-proxy.dev.databricks.com/svg-path-sdf/-/svg-path-sdf-1.1.3.tgz", - "integrity": "sha512-vJJjVq/R5lSr2KLfVXVAStktfcfa1pNFjFOgyJnzZFXlO/fDZ5DmM8FpnSKKzLPfEYTVeXuVBTHF296TpxuJVg==", "license": "MIT", "dependencies": { "bitmap-sdf": "^1.0.0", @@ -8572,8 +7366,6 @@ }, "node_modules/tailwind-merge": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", - "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", "license": "MIT", "funding": { "type": "github", @@ -8582,14 +7374,10 @@ }, "node_modules/tailwindcss": { "version": "4.1.17", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", - "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", "license": "MIT" }, "node_modules/tailwindcss-animate": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", - "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", "license": "MIT", "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" @@ -8597,8 +7385,6 @@ }, "node_modules/tapable": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true, "license": "MIT", "engines": { @@ -8611,8 +7397,6 @@ }, "node_modules/through2": { "version": "2.0.5", - "resolved": "https://npm-proxy.dev.databricks.com/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "license": "MIT", "dependencies": { "readable-stream": "~2.3.6", @@ -8621,26 +7405,18 @@ }, "node_modules/tiny-invariant": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, "node_modules/tiny-warning": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", "license": "MIT" }, "node_modules/tinycolor2": { "version": "1.6.0", - "resolved": "https://npm-proxy.dev.databricks.com/tinycolor2/-/tinycolor2-1.6.0.tgz", - "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", "license": "MIT" }, "node_modules/tinyglobby": { "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -8655,8 +7431,6 @@ }, "node_modules/tinyglobby/node_modules/fdir": { "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "license": "MIT", "engines": { "node": ">=12.0.0" @@ -8672,8 +7446,6 @@ }, "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" @@ -8684,20 +7456,14 @@ }, "node_modules/tinyqueue": { "version": "2.0.3", - "resolved": "https://npm-proxy.dev.databricks.com/tinyqueue/-/tinyqueue-2.0.3.tgz", - "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==", "license": "ISC" }, "node_modules/to-float32": { "version": "1.1.0", - "resolved": "https://npm-proxy.dev.databricks.com/to-float32/-/to-float32-1.1.0.tgz", - "integrity": "sha512-keDnAusn/vc+R3iEiSDw8TOF7gPiTLdK1ArvWtYbJQiVfmRg6i/CAvbKq3uIS0vWroAC7ZecN3DjQKw3aSklUg==", "license": "MIT" }, "node_modules/to-px": { "version": "1.0.1", - "resolved": "https://npm-proxy.dev.databricks.com/to-px/-/to-px-1.0.1.tgz", - "integrity": "sha512-2y3LjBeIZYL19e5gczp14/uRWFDtDUErJPVN3VU9a7SJO+RjGRtYR47aMN2bZgGlxvW4ZcEz2ddUPVHXcMfuXw==", "license": "MIT", "dependencies": { "parse-unit": "^1.0.1" @@ -8705,8 +7471,6 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -8717,8 +7481,6 @@ }, "node_modules/topojson-client": { "version": "3.1.0", - "resolved": "https://npm-proxy.dev.databricks.com/topojson-client/-/topojson-client-3.1.0.tgz", - "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", "license": "ISC", "dependencies": { "commander": "2" @@ -8731,8 +7493,6 @@ }, "node_modules/trim-lines": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", "dev": true, "license": "MIT", "funding": { @@ -8742,8 +7502,6 @@ }, "node_modules/ts-api-utils": { "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { @@ -8755,14 +7513,10 @@ }, "node_modules/tslib": { "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/tsx": { "version": "4.20.6", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", - "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "license": "MIT", "dependencies": { "esbuild": "~0.25.0", @@ -8780,8 +7534,6 @@ }, "node_modules/tw-animate-css": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", - "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Wombosvideo" @@ -8789,14 +7541,10 @@ }, "node_modules/type": { "version": "2.7.3", - "resolved": "https://npm-proxy.dev.databricks.com/type/-/type-2.7.3.tgz", - "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", "license": "ISC" }, "node_modules/type-check": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { @@ -8808,14 +7556,10 @@ }, "node_modules/typedarray": { "version": "0.0.6", - "resolved": "https://npm-proxy.dev.databricks.com/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, "node_modules/typedarray-pool": { "version": "1.2.0", - "resolved": "https://npm-proxy.dev.databricks.com/typedarray-pool/-/typedarray-pool-1.2.0.tgz", - "integrity": "sha512-YTSQbzX43yvtpfRtIDAYygoYtgT+Rpjuxy9iOpczrjpXLgGoyG7aS5USJXV2d3nn8uHTeb9rXDvzS27zUg5KYQ==", "license": "MIT", "dependencies": { "bit-twiddle": "^1.0.0", @@ -8824,8 +7568,6 @@ }, "node_modules/typescript": { "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -8838,8 +7580,6 @@ }, "node_modules/typescript-eslint": { "version": "8.45.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.45.0.tgz", - "integrity": "sha512-qzDmZw/Z5beNLUrXfd0HIW6MzIaAV5WNDxmMs9/3ojGOpYavofgNAAD/nC6tGV2PczIi0iw8vot2eAe/sBn7zg==", "dev": true, "license": "MIT", "dependencies": { @@ -8862,15 +7602,11 @@ }, "node_modules/undici-types": { "version": "7.13.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz", - "integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==", "devOptional": true, "license": "MIT" }, "node_modules/unist-util-is": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", - "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", "dev": true, "license": "MIT", "dependencies": { @@ -8883,8 +7619,6 @@ }, "node_modules/unist-util-position": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", "dev": true, "license": "MIT", "dependencies": { @@ -8897,8 +7631,6 @@ }, "node_modules/unist-util-stringify-position": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8911,8 +7643,6 @@ }, "node_modules/unist-util-visit": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", - "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", "dev": true, "license": "MIT", "dependencies": { @@ -8927,8 +7657,6 @@ }, "node_modules/unist-util-visit-parents": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", - "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8942,8 +7670,6 @@ }, "node_modules/unplugin": { "version": "2.3.10", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.10.tgz", - "integrity": "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", @@ -8957,8 +7683,6 @@ }, "node_modules/unplugin/node_modules/picomatch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" @@ -8969,14 +7693,10 @@ }, "node_modules/unquote": { "version": "1.1.1", - "resolved": "https://npm-proxy.dev.databricks.com/unquote/-/unquote-1.1.1.tgz", - "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==", "license": "MIT" }, "node_modules/update-browserslist-db": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "funding": [ { "type": "opencollective", @@ -9005,14 +7725,10 @@ }, "node_modules/update-diff": { "version": "1.1.0", - "resolved": "https://npm-proxy.dev.databricks.com/update-diff/-/update-diff-1.1.0.tgz", - "integrity": "sha512-rCiBPiHxZwT4+sBhEbChzpO5hYHjm91kScWgdHf4Qeafs6Ba7MBl+d9GlGv72bcTZQO0sLmtQS1pHSWoCLtN/A==", "license": "MIT" }, "node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -9021,8 +7737,6 @@ }, "node_modules/use-callback-ref": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -9042,8 +7756,6 @@ }, "node_modules/use-sidecar": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", "license": "MIT", "dependencies": { "detect-node-es": "^1.1.0", @@ -9064,8 +7776,6 @@ }, "node_modules/use-sync-external-store": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -9073,14 +7783,10 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", - "resolved": "https://npm-proxy.dev.databricks.com/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, "node_modules/vfile": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", "dev": true, "license": "MIT", "dependencies": { @@ -9094,8 +7800,6 @@ }, "node_modules/vfile-message": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", "dev": true, "license": "MIT", "dependencies": { @@ -9109,8 +7813,6 @@ }, "node_modules/victory-vendor": { "version": "37.3.6", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", - "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", "license": "MIT AND ISC", "dependencies": { "@types/d3-array": "^3.0.3", @@ -9132,8 +7834,6 @@ "node_modules/vite": { "name": "rolldown-vite", "version": "7.1.14", - "resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.1.14.tgz", - "integrity": "sha512-eSiiRJmovt8qDJkGyZuLnbxAOAdie6NCmmd0NkTC0RJI9duiSBTfr8X2mBYJOUFzxQa2USaHmL99J9uMxkjCyw==", "license": "MIT", "dependencies": { "@oxc-project/runtime": "0.92.0", @@ -9207,8 +7907,6 @@ }, "node_modules/vite/node_modules/fdir": { "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "license": "MIT", "engines": { "node": ">=12.0.0" @@ -9224,8 +7922,6 @@ }, "node_modules/vite/node_modules/picomatch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" @@ -9236,8 +7932,6 @@ }, "node_modules/vt-pbf": { "version": "3.1.3", - "resolved": "https://npm-proxy.dev.databricks.com/vt-pbf/-/vt-pbf-3.1.3.tgz", - "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", "license": "MIT", "dependencies": { "@mapbox/point-geometry": "0.1.0", @@ -9247,14 +7941,10 @@ }, "node_modules/weak-map": { "version": "1.0.8", - "resolved": "https://npm-proxy.dev.databricks.com/weak-map/-/weak-map-1.0.8.tgz", - "integrity": "sha512-lNR9aAefbGPpHO7AEnY0hCFjz1eTkWCXYvkTRrTHs9qv8zJp+SkVYpzfLIFXQQiG3tVvbNFQgVg2bQS8YGgxyw==", "license": "Apache-2.0" }, "node_modules/webgl-context": { "version": "2.2.0", - "resolved": "https://npm-proxy.dev.databricks.com/webgl-context/-/webgl-context-2.2.0.tgz", - "integrity": "sha512-q/fGIivtqTT7PEoF07axFIlHNk/XCPaYpq64btnepopSWvKNFkoORlQYgqDigBIuGA1ExnFd/GnSUnBNEPQY7Q==", "license": "MIT", "dependencies": { "get-canvas-context": "^1.0.1" @@ -9262,14 +7952,10 @@ }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", - "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", - "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "license": "MIT" }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { @@ -9284,8 +7970,6 @@ }, "node_modules/word-wrap": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { @@ -9294,8 +7978,6 @@ }, "node_modules/world-calendars": { "version": "1.0.4", - "resolved": "https://npm-proxy.dev.databricks.com/world-calendars/-/world-calendars-1.0.4.tgz", - "integrity": "sha512-VGRnLJS+xJmGDPodgJRnGIDwGu0s+Cr9V2HB3EzlDZ5n0qb8h5SJtGUEkjrphZYAglEiXZ6kiXdmk0H/h/uu/w==", "license": "MIT", "dependencies": { "object-assign": "^4.1.0" @@ -9303,14 +7985,10 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://npm-proxy.dev.databricks.com/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, "node_modules/xtend": { "version": "4.0.2", - "resolved": "https://npm-proxy.dev.databricks.com/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "license": "MIT", "engines": { "node": ">=0.4" @@ -9318,8 +7996,6 @@ }, "node_modules/y18n": { "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, "license": "ISC", "engines": { @@ -9328,14 +8004,10 @@ }, "node_modules/yallist": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "license": "ISC" }, "node_modules/yargs": { "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "license": "MIT", "dependencies": { @@ -9353,8 +8025,6 @@ }, "node_modules/yargs-parser": { "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", "engines": { @@ -9363,8 +8033,6 @@ }, "node_modules/yargs/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -9373,15 +8041,11 @@ }, "node_modules/yargs/node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, "node_modules/yargs/node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -9395,8 +8059,6 @@ }, "node_modules/yargs/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -9408,8 +8070,6 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { @@ -9421,8 +8081,6 @@ }, "node_modules/zod": { "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -9430,35 +8088,12 @@ }, "node_modules/zwitch": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", "dev": true, "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } - }, - "package": { - "name": "@databricks/appkit-ui", - "version": "1.0.0", - "extraneous": true, - "dependencies": { - "clsx": "^2.1.1", - "tailwind-merge": "^3.4.0" - }, - "devDependencies": { - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "recharts": "^3.4.1" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0", - "recharts": "^2.0.0 || ^3.0.0" - } } } } From 234c251a2fcc0b9d9142de7dff2689039ea1f3f0 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Mon, 4 May 2026 14:56:49 +0200 Subject: [PATCH 20/34] fix(playground): use waitForRequest instead of page.on for metric assertions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous test attached `page.on("request")` after `beforeEach` had already registered `page.route()` mocks. With Playwright's per-page listener attachment combined with same-page route mocks fulfilling synchronously, the `request` event for the very first navigation can slip past a listener attached inside the test body — captured 0 calls even though all 4 sibling tests in the same file rely on those exact requests firing (chart renders, OBO error banner, etc.). `waitForRequest` is the established idiom in this repo (see reconnect.spec.ts) and is the form Playwright reliably fires for mocked routes — the matcher is registered as a one-shot promise before the navigation triggers it, so listener-attachment ordering cannot drop events. Each await doubles as the "received >= 1" assertion: it throws on timeout otherwise. Co-authored-by: Isaac --- apps/dev-playground/tests/metrics.spec.ts | 31 ++++++++++------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/apps/dev-playground/tests/metrics.spec.ts b/apps/dev-playground/tests/metrics.spec.ts index fa974784c..2092a7afb 100644 --- a/apps/dev-playground/tests/metrics.spec.ts +++ b/apps/dev-playground/tests/metrics.spec.ts @@ -135,25 +135,22 @@ test.describe("Metric Views Route Tests", () => { }); test("calls expected metric endpoints on page load", async ({ page }) => { - const calls: string[] = []; - page.on("request", (request) => { - if (request.url().includes("/api/analytics/metric/")) { - calls.push(request.url()); - } - }); - - await page.goto("/metrics", { waitUntil: "networkidle" }); - - // React 19 Strict Mode doubles useEffect invocations in dev mode; assert - // both routes fire (allowing for the multiplier). - const revenueCalls = calls.filter((u) => - u.endsWith("/api/analytics/metric/revenue"), + // Match the codebase idiom for "this navigation should fire request X" — + // see reconnect.spec.ts. `waitForRequest` registers a one-shot matcher + // before the navigation triggers it, so listener-attachment ordering + // (a known sharp edge with `page.on("request")` plus `page.route()` mocks + // attached in beforeEach) is no longer in play. Each await doubles as the + // "received >= 1" assertion — it throws on timeout otherwise. + const revenuePromise = page.waitForRequest((req) => + req.url().endsWith("/api/analytics/metric/revenue"), ); - const customerCalls = calls.filter((u) => - u.endsWith("/api/analytics/metric/customer_metrics"), + const customerPromise = page.waitForRequest((req) => + req.url().endsWith("/api/analytics/metric/customer_metrics"), ); - expect(revenueCalls.length).toBeGreaterThanOrEqual(1); - expect(customerCalls.length).toBeGreaterThanOrEqual(1); + await page.goto("/metrics", { waitUntil: "networkidle" }); + + await revenuePromise; + await customerPromise; }); }); From 38bf39a4c0828b341926283a4d725a3cbf2ac2bd Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Mon, 4 May 2026 18:28:36 +0200 Subject: [PATCH 21/34] fix(appkit): server-side scrub of metric route errors Two security blockers from the round-2 review: - The catch fallback in `_handleMetricRoute` was echoing `err.message` for any non-AppKitError throw site. Hard-code the public message; route the raw detail to telemetry only. Closes the recurring `error-messages-leaking-details` pattern at the validation boundary. - The SSE error envelope was forwarding raw warehouse errors verbatim for 4xx-shaped responses (e.g. `[TABLE_OR_VIEW_NOT_FOUND] catalog.schema.fqn`). The round-1 client-side scrub depends on the React hook running; any non-React consumer (curl, alternate SDK) would still see the raw text. Wrap the executeStream callback so any error inside the metric query is caught, telemetered with full detail, and re-thrown as a sanitized ExecutionError. Production gets a generic message; dev keeps the original for diagnostics. Co-authored-by: Isaac --- .../appkit/src/plugins/analytics/analytics.ts | 61 +++++++++++++++---- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/packages/appkit/src/plugins/analytics/analytics.ts b/packages/appkit/src/plugins/analytics/analytics.ts index 096b34052..6b680daae 100644 --- a/packages/appkit/src/plugins/analytics/analytics.ts +++ b/packages/appkit/src/plugins/analytics/analytics.ts @@ -8,7 +8,7 @@ import type { } from "shared"; import { SQLWarehouseConnector } from "../../connectors"; import { getWarehouseId, getWorkspaceClient } from "../../context"; -import { AppKitError } from "../../errors"; +import { AppKitError, ExecutionError } from "../../errors"; import { createLogger } from "../../logging/logger"; import { Plugin, toPlugin } from "../../plugin"; import type { PluginManifest } from "../../registry"; @@ -349,10 +349,20 @@ export class AnalyticsPlugin extends Plugin { } // Validator throws ValidationError; asUser/resolveUserId throw // AuthenticationError — both are AppKitError. This branch only fires - // for unexpected errors; keep generic to avoid leaking internals. - res.status(400).json({ - error: err instanceof Error ? err.message : "Invalid request body", + // for unexpected errors. Hard-code the public message (do not echo + // err.message — it could contain stack-adjacent internals from any + // unwrapped throw site). The full detail goes to telemetry only. + event?.setContext("analytics", { + unexpected_error: err instanceof Error ? err.message : String(err), + metric_key: key, }); + logger.warn( + req, + "Unexpected throw during metric request setup for %s: %s", + key, + err instanceof Error ? err.message : String(err), + ); + res.status(400).json({ error: "Invalid request body" }); return; } @@ -395,14 +405,41 @@ export class AnalyticsPlugin extends Plugin { await executor.executeStream( res, async (signal) => { - const { statement, parameters } = buildMetricSql(registration, request); - const result = await executor.query( - statement, - Object.keys(parameters).length > 0 ? parameters : undefined, - queryParameters.formatParameters, - signal, - ); - return { type: queryParameters.type, ...result }; + try { + const { statement, parameters } = buildMetricSql( + registration, + request, + ); + const result = await executor.query( + statement, + Object.keys(parameters).length > 0 ? parameters : undefined, + queryParameters.formatParameters, + signal, + ); + return { type: queryParameters.type, ...result }; + } catch (err) { + // Server-side scrub for the SSE error envelope. Without this, any + // 4xx from the warehouse (e.g. TABLE_OR_VIEW_NOT_FOUND with a UC + // FQN attached) flows verbatim through the framework's pass-through + // for client-status errors — visible to anyone hitting the route, + // including non-React consumers that the round-1 client-side scrub + // does not protect. Production gets a generic message; dev keeps + // the original for diagnostics. Telemetry always carries the raw. + event?.setContext("analytics", { + metric_query_error: + err instanceof Error ? err.message : String(err), + metric_key: key, + }); + if (err instanceof AppKitError) throw err; + const isProd = process.env.NODE_ENV === "production"; + throw new ExecutionError( + isProd + ? "Failed to execute metric query" + : err instanceof Error + ? err.message + : "Failed to execute metric query", + ); + } }, streamExecutionSettings, executorKey, From f91e910313ae39a08e9cf1131f6071b3fe3cc53f Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Mon, 4 May 2026 18:28:55 +0200 Subject: [PATCH 22/34] fix(appkit, appkit-ui): cap unbounded inputs and memoize hot-path allocations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three blockers from the round-2 review: - Cap `measures`, `dimensions`, `filter.values`, and `limit` at the validator level. Closes the recurring `unbounded-request-parameters` pattern (3+ past reviews) — without these caps, a hostile caller could send `values: [...10M items...]` and force the validator + named bind-var step to chew through unbounded work. Limits are deliberately generous (50 measures, 20 dimensions, 1000 filter values, 100k row limit) so legitimate BI traffic never trips them. - Memoize the per-registration Zod schema via a WeakMap. `validateMetricRequest` was rebuilding a recursive schema with ~10 chained refinements on every request. The WeakMap lets the cache empty automatically when the registry is reloaded (dev hot-reload of metric.json) — old registration objects become unreferenced and the entry is GC'd. - Memoize `Intl.NumberFormat` and parsed format specs in formatValue. Charts/tables call this per cell; allocation is notoriously slow in V8. A 1000-row × 5-col table was paying ~5000 instantiations per render. Module-level Maps keyed on serialized options / format strings; cardinality is tiny in practice (one entry per metric-view measure with a format). Co-authored-by: Isaac --- packages/appkit-ui/src/format/format.ts | 53 +++++++++++++++--- .../appkit/src/plugins/analytics/metric.ts | 54 +++++++++++++++++-- .../plugins/analytics/tests/metric.test.ts | 29 ++++++++++ 3 files changed, 126 insertions(+), 10 deletions(-) diff --git a/packages/appkit-ui/src/format/format.ts b/packages/appkit-ui/src/format/format.ts index d38b0f432..6bc5ea677 100644 --- a/packages/appkit-ui/src/format/format.ts +++ b/packages/appkit-ui/src/format/format.ts @@ -1,5 +1,36 @@ import type { ColumnMetadata, FormatSpec } from "./types"; +/** + * Module-level cache for parsed format specs. Format strings are pinned by + * the metric view's YAML and reused for every cell render — without caching, + * `parseFormatSpec` runs ~5 regex matches per call. Cardinality in + * production is tiny (one entry per measure/dim column with a format), so + * an unbounded `Map` is safe; the cache lives for the lifetime of the + * module and clears with the page. + */ +const parsedFormatCache = new Map(); + +/** + * Module-level cache for `Intl.NumberFormat` instances. Allocation is + * notoriously slow in V8, and chart cells call `formatValue` per row. A + * 1000-row × 5-column table would otherwise pay ~5000 instantiations per + * render. Keyed on a stringified options bundle so identical option sets + * share an instance. + */ +const numberFormatCache = new Map(); + +function getNumberFormat(options: Intl.NumberFormatOptions): Intl.NumberFormat { + // Locale is left at the runtime default (`undefined`) — same as the + // pre-cache code — so options serialization is the only key dimension. + const key = JSON.stringify(options); + let fmt = numberFormatCache.get(key); + if (fmt === undefined) { + fmt = new Intl.NumberFormat(undefined, options); + numberFormatCache.set(key, fmt); + } + return fmt; +} + /** * Library-agnostic format utilities for UC Metric View consumption. * @@ -67,9 +98,7 @@ export function formatValue(value: unknown, format?: FormatSpec): string { // No format / unrecognized format → localized number formatting. Using // the user's locale (no explicit "en-US") so numbers render correctly in // EU/JP/etc apps without the customer wiring locale plumbing. - return new Intl.NumberFormat(undefined, { - maximumFractionDigits: 6, - }).format(numeric); + return getNumberFormat({ maximumFractionDigits: 6 }).format(numeric); } const { kind, fractionDigits, useGrouping, currencyPrefix, currencySuffix } = @@ -77,7 +106,7 @@ export function formatValue(value: unknown, format?: FormatSpec): string { switch (kind) { case "percent": - return new Intl.NumberFormat(undefined, { + return getNumberFormat({ style: "percent", minimumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits, @@ -89,7 +118,7 @@ export function formatValue(value: unknown, format?: FormatSpec): string { // — the YAML's `$#,##0.00` does not specify ISO currency code, and // assuming USD would be wrong for non-US deployments. Passthrough lets // data engineers pin the symbol they intend. - const numberPart = new Intl.NumberFormat(undefined, { + const numberPart = getNumberFormat({ minimumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits, useGrouping, @@ -98,7 +127,7 @@ export function formatValue(value: unknown, format?: FormatSpec): string { return `${sign}${currencyPrefix ?? ""}${numberPart}${currencySuffix ?? ""}`; } case "number": - return new Intl.NumberFormat(undefined, { + return getNumberFormat({ minimumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits, useGrouping, @@ -211,8 +240,20 @@ interface ParsedFormat { * Approach: strip percent / currency markers, count fractional digits via * the substring after `.`, detect grouping via the presence of `,`. Anything * not matching the recognized shape returns null. + * + * Result is memoized in {@link parsedFormatCache} — format strings are + * pinned by the metric view's YAML and reused for every cell render, so we + * pay the regex cost once per distinct spec. */ function parseFormatSpec(spec: FormatSpec): ParsedFormat | null { + const cached = parsedFormatCache.get(spec); + if (cached !== undefined) return cached; + const result = parseFormatSpecImpl(spec); + parsedFormatCache.set(spec, result); + return result; +} + +function parseFormatSpecImpl(spec: FormatSpec): ParsedFormat | null { const trimmed = spec.trim(); if (trimmed.length === 0) return null; diff --git a/packages/appkit/src/plugins/analytics/metric.ts b/packages/appkit/src/plugins/analytics/metric.ts index ce2a69a77..9331c9463 100644 --- a/packages/appkit/src/plugins/analytics/metric.ts +++ b/packages/appkit/src/plugins/analytics/metric.ts @@ -47,6 +47,19 @@ const METRIC_FILTER_OPERATORS = [ */ const METRIC_FILTER_MAX_DEPTH = 8; +/** + * Cardinality caps on user-controlled arrays. Closes the recurring + * `unbounded-request-parameters` finding: a hostile caller could otherwise + * send `values: [...10M items...]` and exhaust the validator + the named + * bind-var binding step. The limits below are deliberately generous — + * higher than any real BI UI would emit — so legitimate traffic never trips + * them. If a customer scenario needs more, expose a per-metric override. + */ +const METRIC_MEASURES_MAX = 50; +const METRIC_DIMENSIONS_MAX = 20; +const METRIC_FILTER_VALUES_MAX = 1000; +const METRIC_LIMIT_MAX = 100_000; + /** * Range ops — require numeric or date-typed dimensions. The remaining ops * split into: @@ -422,7 +435,12 @@ export function makeMetricRequestSchema( operator: z.string().min(1, { message: "filter predicate 'operator' cannot be empty", }) as z.ZodType, - values: z.array(z.union([z.string(), z.number()])).optional(), + values: z + .array(z.union([z.string(), z.number()])) + .max(METRIC_FILTER_VALUES_MAX, { + message: `filter predicate 'values' length exceeds the maximum of ${METRIC_FILTER_VALUES_MAX}`, + }) + .optional(), }) .strict(); @@ -448,8 +466,16 @@ export function makeMetricRequestSchema( .object({ measures: z .array(measureItemSchema) - .min(1, { message: "measures must contain at least one entry" }), - dimensions: z.array(dimensionItemSchema).optional(), + .min(1, { message: "measures must contain at least one entry" }) + .max(METRIC_MEASURES_MAX, { + message: `measures length exceeds the maximum of ${METRIC_MEASURES_MAX}`, + }), + dimensions: z + .array(dimensionItemSchema) + .max(METRIC_DIMENSIONS_MAX, { + message: `dimensions length exceeds the maximum of ${METRIC_DIMENSIONS_MAX}`, + }) + .optional(), timeGrain: timeGrainSchema.optional(), filter: filterSchema.optional(), format: z.enum(["JSON", "ARROW"]).optional(), @@ -457,6 +483,9 @@ export function makeMetricRequestSchema( .number() .int({ message: "limit must be an integer" }) .positive({ message: "limit must be positive" }) + .max(METRIC_LIMIT_MAX, { + message: `limit exceeds the maximum of ${METRIC_LIMIT_MAX}`, + }) .optional(), }) .strict(); @@ -785,11 +814,28 @@ function collectAllowedGrains(grainsByDim: Record): string[] { * `context.issues` for server-side telemetry. This prevents an unauthenticated * caller from enumerating the registered schema by sending malformed bodies. */ +/** + * Per-registration Zod schema cache. The schema is recursive (filter tree + * with `z.lazy`) and constructs ~10 chained refinements, which is non-trivial + * to rebuild on every request. Keyed on the registration object so the cache + * empties automatically when the registry is reloaded (e.g., dev hot-reload + * of `metric.json`) — old registration objects become unreferenced and the + * `WeakMap` entry is garbage-collected. + */ +const metricRequestSchemaCache = new WeakMap< + MetricRegistration, + z.ZodType +>(); + export function validateMetricRequest( registration: MetricRegistration, body: unknown, ): IAnalyticsMetricRequest { - const schema = makeMetricRequestSchema(registration); + let schema = metricRequestSchemaCache.get(registration); + if (schema === undefined) { + schema = makeMetricRequestSchema(registration); + metricRequestSchemaCache.set(registration, schema); + } const result = schema.safeParse(body); if (!result.success) { const fieldPaths = result.error.issues diff --git a/packages/appkit/src/plugins/analytics/tests/metric.test.ts b/packages/appkit/src/plugins/analytics/tests/metric.test.ts index cf8a1481c..7fb0e60e1 100644 --- a/packages/appkit/src/plugins/analytics/tests/metric.test.ts +++ b/packages/appkit/src/plugins/analytics/tests/metric.test.ts @@ -129,6 +129,35 @@ describe("metric — pure helpers", () => { ).toThrowError(/fields:.*limit/); }); + test("rejects limit exceeding the cap (unbounded-request-parameters)", () => { + // Recurring pattern from prior reviews — caps prevent a hostile caller + // from passing absurdly large `limit` values that would force the + // warehouse to materialize unbounded result sets. + expect(() => + validateMetricRequest(REVENUE_REGISTRATION, { + measures: ["arr"], + limit: 10_000_000, + }), + ).toThrowError(/fields:.*limit/); + }); + + test("rejects measures exceeding the cap", () => { + const tooMany = Array.from({ length: 100 }, () => "arr"); + expect(() => + validateMetricRequest(REVENUE_REGISTRATION, { measures: tooMany }), + ).toThrowError(/fields:.*measures/); + }); + + test("rejects a filter predicate with too many values (DoS guard)", () => { + const big = Array.from({ length: 2000 }, (_, i) => `v${i}`); + expect(() => + validateMetricRequest(REVENUE_REGISTRATION, { + measures: ["arr"], + filter: { member: "region", operator: "in", values: big }, + }), + ).toThrowError(/fields:.*filter\.values/); + }); + test("rejects unknown top-level fields (strict)", () => { expect(() => validateMetricRequest(REVENUE_REGISTRATION, { From 32fcd0dc51c09c37e2e244be5cfda50301c8bbf0 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Mon, 4 May 2026 18:53:20 +0200 Subject: [PATCH 23/34] fix(appkit): tighten metric route error scrubbing and 404 disclosure Round-3 review surfaced four issues in the catch I added in 38bf39a4 and the surrounding error-response shape. All confirmed by the recurring error-messages-leaking-details pattern (3+ past reviews): - Flip NODE_ENV polarity to fail-closed. The previous check `process.env.NODE_ENV === "production"` left the prod scrub disabled in any environment that doesn't set NODE_ENV explicitly (containers, serverless). Switch to `!== "development"` so anything but explicit dev mode treats as prod. - Stop fast-pathing 5xx-class AppKitErrors past the scrub. The SQL connector throws ExecutionError (statusCode 500) on warehouse failures, and its message carries raw warehouse text including catalog/schema FQNs. Sanitize 5xx AppKitErrors in production; let 4xx-class through (ValidationError/AuthenticationError messages are constructed by us with known-clean content). - Let AbortError pass through unwrapped. The new try/catch was swallowing client-driven cancellations and re-emitting them as ExecutionError, polluting telemetry and changing the framework's cancellation semantics. - Drop the user-supplied key from the 404 body. Echoing `Metric "X" not registered` lets an unauthenticated probe enumerate registered keys by elimination. The 404 status stays for tooling; the body becomes a generic "Metric not found" and the actual key goes to telemetry only. Co-authored-by: Isaac --- .../appkit/src/plugins/analytics/analytics.ts | 33 +++++++++++++++++-- .../plugins/analytics/tests/metric.test.ts | 9 +++-- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/packages/appkit/src/plugins/analytics/analytics.ts b/packages/appkit/src/plugins/analytics/analytics.ts index 6b680daae..7df357f53 100644 --- a/packages/appkit/src/plugins/analytics/analytics.ts +++ b/packages/appkit/src/plugins/analytics/analytics.ts @@ -291,7 +291,13 @@ export class AnalyticsPlugin extends Plugin { const registration = this.metricRegistry[key]; if (!registration) { - res.status(404).json({ error: `Metric "${key}" not registered` }); + // Don't echo the user-supplied `key` back in the public response. + // Confirming "metric X is not registered" lets an unauthenticated + // probe enumerate registered keys by elimination. The 404 status + // stays — it's useful for tooling — but the body is generic; full + // detail goes to telemetry only. + event?.setContext("analytics", { unknown_metric_key: key }); + res.status(404).json({ error: "Metric not found" }); return; } @@ -418,6 +424,12 @@ export class AnalyticsPlugin extends Plugin { ); return { type: queryParameters.type, ...result }; } catch (err) { + // Cancellation must pass through unwrapped so the framework's + // stream layer can distinguish client-driven aborts from real + // failures (different telemetry, no error event emitted). + if (err instanceof Error && err.name === "AbortError") { + throw err; + } // Server-side scrub for the SSE error envelope. Without this, any // 4xx from the warehouse (e.g. TABLE_OR_VIEW_NOT_FOUND with a UC // FQN attached) flows verbatim through the framework's pass-through @@ -425,13 +437,28 @@ export class AnalyticsPlugin extends Plugin { // including non-React consumers that the round-1 client-side scrub // does not protect. Production gets a generic message; dev keeps // the original for diagnostics. Telemetry always carries the raw. + // + // Fail-closed env check: only an explicit "development" treats as + // dev. Containers / serverless runtimes that leave NODE_ENV unset + // must not leak warehouse internals. event?.setContext("analytics", { metric_query_error: err instanceof Error ? err.message : String(err), metric_key: key, }); - if (err instanceof AppKitError) throw err; - const isProd = process.env.NODE_ENV === "production"; + const isProd = process.env.NODE_ENV !== "development"; + if (err instanceof AppKitError) { + // 5xx-class AppKitErrors (notably ExecutionError raised by the + // SQL connector on warehouse failures) carry raw warehouse text + // in their message. Scrub those in prod; let 4xx-class + // AppKitErrors (ValidationError, AuthenticationError) through + // since their messages are constructed by us with known-clean + // content. + if (isProd && err.statusCode >= 500) { + throw new ExecutionError("Failed to execute metric query"); + } + throw err; + } throw new ExecutionError( isProd ? "Failed to execute metric query" diff --git a/packages/appkit/src/plugins/analytics/tests/metric.test.ts b/packages/appkit/src/plugins/analytics/tests/metric.test.ts index 7fb0e60e1..7ddda653a 100644 --- a/packages/appkit/src/plugins/analytics/tests/metric.test.ts +++ b/packages/appkit/src/plugins/analytics/tests/metric.test.ts @@ -878,9 +878,12 @@ describe("AnalyticsPlugin — metric route handler", () => { await handler(mockReq, mockRes); expect(mockRes.status).toHaveBeenCalledWith(404); - expect(mockRes.json).toHaveBeenCalledWith({ - error: 'Metric "ghost" not registered', - }); + const errorPayload = (mockRes.json as any).mock.calls[0][0]; + expect(errorPayload.error).toBe("Metric not found"); + // Defense-in-depth: the public 404 must not echo the user-supplied key + // back. Confirming "metric X is not registered" lets unauthenticated + // probes enumerate registered keys by elimination. + expect(errorPayload.error).not.toMatch(/ghost/); }); test("returns 503 when the registered metric has no build-time metadata (fail-closed)", async () => { From 11203774127f94cc7e31f50a3942f5ad85855329 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Mon, 4 May 2026 18:54:13 +0200 Subject: [PATCH 24/34] fix(appkit): pre-parse filter depth cap to prevent stack-overflow DoS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zod's union/object parsers walk the filter tree recursively (via z.lazy) before our `validateFilterTree` superRefine depth cap fires. A pathologically deep `{ and: [{ and: [...] }] }` payload — say 10000 levels — would stack-overflow the Node process during parse, never reaching the validator's depth check. Add an iterative pre-parse walk in `validateMetricRequest` that uses an explicit stack and aborts as soon as `METRIC_FILTER_MAX_DEPTH` is exceeded. Predicate leaves don't count toward depth — only nested `and` / `or` wrappers, matching the existing in-tree validator's rule. Closes the recurring `unbounded-request-parameters` pattern at the call-stack vector that round-2's array-size caps did not cover. New regression test: 10000-deep AND tree previously crashed the worker; now rejects cleanly with the canonical "fields: filter" 400. Co-authored-by: Isaac --- .../appkit/src/plugins/analytics/metric.ts | 48 +++++++++++++++++++ .../plugins/analytics/tests/metric.test.ts | 21 ++++++++ 2 files changed, 69 insertions(+) diff --git a/packages/appkit/src/plugins/analytics/metric.ts b/packages/appkit/src/plugins/analytics/metric.ts index 9331c9463..b5cfe8676 100644 --- a/packages/appkit/src/plugins/analytics/metric.ts +++ b/packages/appkit/src/plugins/analytics/metric.ts @@ -827,10 +827,58 @@ const metricRequestSchemaCache = new WeakMap< z.ZodType >(); +/** + * Iterative pre-parse depth check. Zod's union/object parsers walk the input + * recursively before our `superRefine` depth cap fires, so a deeply-nested + * `{ and: [{ and: [...] }] }` payload could stack-overflow during parse, + * never reaching the validator's depth check. This walk is iterative (uses + * an explicit stack) and aborts as soon as `METRIC_FILTER_MAX_DEPTH` is + * exceeded, so a hostile payload of any size cannot drive the call stack. + * + * Predicate leaves do NOT count toward depth — only nested `and` / `or` + * wrappers — matching the rule the in-tree validator enforces in + * {@link validateFilterTree}. + */ +function preCheckFilterDepth(filter: unknown): void { + if (filter == null || typeof filter !== "object") return; + const stack: Array<[unknown, number]> = [[filter, 0]]; + while (stack.length > 0) { + const popped = stack.pop(); + if (popped === undefined) continue; + const [node, depth] = popped; + if (node == null || typeof node !== "object") continue; + const obj = node as Record; + let children: unknown[] | null = null; + if (Array.isArray(obj.and)) children = obj.and; + else if (Array.isArray(obj.or)) children = obj.or; + if (children !== null) { + if (depth + 1 > METRIC_FILTER_MAX_DEPTH) { + throw new ValidationError( + "Invalid metric request body (fields: filter)", + { + context: { + reason: `filter AND/OR nesting exceeds the maximum depth of ${METRIC_FILTER_MAX_DEPTH}`, + }, + }, + ); + } + for (const child of children) { + stack.push([child, depth + 1]); + } + } + } +} + export function validateMetricRequest( registration: MetricRegistration, body: unknown, ): IAnalyticsMetricRequest { + // Bound the recursion depth before Zod sees the input — the schema's + // own depth check fires inside `superRefine` which only runs after Zod's + // recursive parse has already walked the tree on the call stack. + if (body != null && typeof body === "object") { + preCheckFilterDepth((body as { filter?: unknown }).filter); + } let schema = metricRequestSchemaCache.get(registration); if (schema === undefined) { schema = makeMetricRequestSchema(registration); diff --git a/packages/appkit/src/plugins/analytics/tests/metric.test.ts b/packages/appkit/src/plugins/analytics/tests/metric.test.ts index 7ddda653a..f63003952 100644 --- a/packages/appkit/src/plugins/analytics/tests/metric.test.ts +++ b/packages/appkit/src/plugins/analytics/tests/metric.test.ts @@ -1704,6 +1704,27 @@ describe("metric — filter translator", () => { ).not.toThrow(); }); + test("rejects pathologically deep filter without stack-overflow (pre-parse cap)", () => { + // Without the iterative pre-parse depth check, Zod's recursive parse + // walks the union/object tree on the call stack BEFORE the validator's + // own depth cap fires inside `superRefine`. A 10000-deep payload would + // stack-overflow the Node process. The pre-walk caps it iteratively. + let node: any = { + member: "region", + operator: "equals", + values: ["EMEA"], + }; + for (let i = 0; i < 10_000; i += 1) { + node = { and: [node] }; + } + expect(() => + validateMetricRequest(REVENUE_PHASE3_REGISTRATION, { + measures: ["arr"], + filter: node, + }), + ).toThrowError(/fields:.*filter/); + }); + test("rejects empty `or` group (empty disjunction is vacuously false)", () => { // Empty AND is vacuously true (no constraint). Empty OR would be // vacuously false — silently dropping the predicate. Force the caller From 9524ae9afad5d73c14a8f968251e66463e03268e Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Mon, 4 May 2026 18:55:14 +0200 Subject: [PATCH 25/34] perf(appkit-ui, appkit): drop Blob allocation; memoize collectAllowedGrains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two small follow-ups from the round-3 perf review: - `useMetricView` was allocating a `Blob` purely to count UTF-8 bytes inside the payload memo. Swap to `new TextEncoder().encode(s).length` — same byte count, no `Blob` object, hot enough on dashboards with many metric tiles to be worth fixing. - `collectAllowedGrains(grainsByDim)` rebuilt a `Set` and re-sorted on every `buildMetricSql` call. The input is static per `MetricRegistration`, so memoize via `WeakMap` keyed on the `grainsByDim` reference. Steady-state SQL construction reuses the sorted array; reload of the registry frees the entry automatically. Co-authored-by: Isaac --- .../src/react/hooks/use-metric-view.ts | 5 ++++- packages/appkit/src/plugins/analytics/metric.ts | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/appkit-ui/src/react/hooks/use-metric-view.ts b/packages/appkit-ui/src/react/hooks/use-metric-view.ts index b790ae2c9..7b243fc43 100644 --- a/packages/appkit-ui/src/react/hooks/use-metric-view.ts +++ b/packages/appkit-ui/src/react/hooks/use-metric-view.ts @@ -121,7 +121,10 @@ export function useMetricView< body.limit = a.limit; } const serialized = JSON.stringify(body); - const sizeInBytes = new Blob([serialized]).size; + // Avoid the Blob allocation just to count bytes — it's a hot path + // on dashboards with many metric tiles. `TextEncoder` is constant- + // time per call and produces the same UTF-8 byte length. + const sizeInBytes = new TextEncoder().encode(serialized).length; if (sizeInBytes > maxParametersSize) { throw new Error( "useMetricView: Request body size exceeds the maximum allowed size", diff --git a/packages/appkit/src/plugins/analytics/metric.ts b/packages/appkit/src/plugins/analytics/metric.ts index b5cfe8676..2aa3045be 100644 --- a/packages/appkit/src/plugins/analytics/metric.ts +++ b/packages/appkit/src/plugins/analytics/metric.ts @@ -788,16 +788,29 @@ function classifyDimensionType(sqlType: string): MetricDimensionTypeClass { * Aggregate the set of allowed time-grains across every time-typed dimension. * * Sorted + deduplicated so the validator's error messages and the cache-key - * construction are deterministic. + * construction are deterministic. Memoized per `grainsByDim` reference: the + * input is static per `MetricRegistration`, so steady-state hits reuse the + * sorted array without re-walking + re-sorting on every `buildMetricSql` + * call. (The validator's path only invokes this once per metric thanks to + * the schema cache; the SQL builder still calls it per request.) */ +const collectAllowedGrainsCache = new WeakMap< + Record, + string[] +>(); + function collectAllowedGrains(grainsByDim: Record): string[] { + const cached = collectAllowedGrainsCache.get(grainsByDim); + if (cached !== undefined) return cached; const set = new Set(); for (const grains of Object.values(grainsByDim)) { for (const g of grains) { set.add(g); } } - return [...set].sort(); + const result = [...set].sort(); + collectAllowedGrainsCache.set(grainsByDim, result); + return result; } /** From 66735b302a852e5cb6290b259d67e1d39aff1021 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Mon, 4 May 2026 19:13:23 +0200 Subject: [PATCH 26/34] fix(appkit): close filter pre-check bypass and add per-group breadth cap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-4 review surfaced a critical bypass in the round-3 pre-check that both reviewers (security + perf) reproduced independently: The previous pre-check used `if (obj.and) ... else if (obj.or) ...` and walked only one branch. A payload like `{ and: [], or: <10000-deep> }` slid past the empty-`and` walk and Zod's union recursion then stack-overflowed on the `or` branch — the exact DoS the pre-check was designed to prevent. Two fixes: - Inspect BOTH `and` and `or` on every node. Zod's `.strict()` will reject the multi-key shape downstream, but the pre-check has to walk every branch Zod might recurse into. - Cap per-group child count at 100 in both the iterative pre-check and the Zod schema. Without it, a flat `{ and: [...10M items...] }` would push 10M frames onto the explicit stack — OOM before validation even reached Zod. Two regression tests: the `{ and: [], or: }` bypass case (which previously crashed with `RangeError: Maximum call stack size exceeded`) and a flat-breadth case to hold the new `.max(100)` group cap. Co-authored-by: Isaac --- .../appkit/src/plugins/analytics/metric.ts | 49 ++++++++++++++++--- .../plugins/analytics/tests/metric.test.ts | 39 +++++++++++++++ 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/packages/appkit/src/plugins/analytics/metric.ts b/packages/appkit/src/plugins/analytics/metric.ts index 2aa3045be..4ee2c0d05 100644 --- a/packages/appkit/src/plugins/analytics/metric.ts +++ b/packages/appkit/src/plugins/analytics/metric.ts @@ -60,6 +60,16 @@ const METRIC_DIMENSIONS_MAX = 20; const METRIC_FILTER_VALUES_MAX = 1000; const METRIC_LIMIT_MAX = 100_000; +/** + * Maximum number of children per AND/OR group node. Without this cap a + * single flat group like `{ and: [...10M empty objects...] }` would push + * tens of millions of frames onto the iterative pre-check's stack — OOM + * before validation even gets to Zod. The Zod schema enforces the same + * cap so the rejection point is consistent regardless of which validator + * catches it first. + */ +const METRIC_FILTER_GROUP_MAX = 100; + /** * Range ops — require numeric or date-typed dimensions. The remaining ops * split into: @@ -449,12 +459,16 @@ export function makeMetricRequestSchema( filterPredicateSchema, z .object({ - and: z.array(filterSchema), + and: z.array(filterSchema).max(METRIC_FILTER_GROUP_MAX, { + message: `filter 'and' group exceeds the maximum of ${METRIC_FILTER_GROUP_MAX} children`, + }), }) .strict(), z .object({ - or: z.array(filterSchema), + or: z.array(filterSchema).max(METRIC_FILTER_GROUP_MAX, { + message: `filter 'or' group exceeds the maximum of ${METRIC_FILTER_GROUP_MAX} children`, + }), }) .strict(), ]), @@ -848,6 +862,16 @@ const metricRequestSchemaCache = new WeakMap< * an explicit stack) and aborts as soon as `METRIC_FILTER_MAX_DEPTH` is * exceeded, so a hostile payload of any size cannot drive the call stack. * + * Walks BOTH `and` and `or` branches when both are present on the same node + * — Zod's `.strict()` will reject the multi-key shape downstream, but the + * pre-check has to inspect every branch Zod might recurse into. An earlier + * version used `else if` and was bypassed by `{ and: [], or: }`. + * + * Group-children breadth is also capped: a flat `{ and: [...10M items...] }` + * payload cannot push 10M frames onto the explicit stack here. The Zod + * schema enforces the same `.max()` so the failure surfaces at the same + * point regardless of which validator catches it first. + * * Predicate leaves do NOT count toward depth — only nested `and` / `or` * wrappers — matching the rule the in-tree validator enforces in * {@link validateFilterTree}. @@ -861,10 +885,23 @@ function preCheckFilterDepth(filter: unknown): void { const [node, depth] = popped; if (node == null || typeof node !== "object") continue; const obj = node as Record; - let children: unknown[] | null = null; - if (Array.isArray(obj.and)) children = obj.and; - else if (Array.isArray(obj.or)) children = obj.or; - if (children !== null) { + // Inspect BOTH `and` and `or` if present. Using `else if` here was a + // critical bypass: a payload of `{ and: [], or: }` slid + // past the pre-check (empty `and` walked, `or` ignored) and Zod's + // union recursion then stack-overflowed on the `or` branch. + for (const groupKey of ["and", "or"] as const) { + const children = obj[groupKey]; + if (!Array.isArray(children)) continue; + if (children.length > METRIC_FILTER_GROUP_MAX) { + throw new ValidationError( + "Invalid metric request body (fields: filter)", + { + context: { + reason: `filter ${groupKey} group has ${children.length} children; the maximum is ${METRIC_FILTER_GROUP_MAX}`, + }, + }, + ); + } if (depth + 1 > METRIC_FILTER_MAX_DEPTH) { throw new ValidationError( "Invalid metric request body (fields: filter)", diff --git a/packages/appkit/src/plugins/analytics/tests/metric.test.ts b/packages/appkit/src/plugins/analytics/tests/metric.test.ts index f63003952..946cf0af0 100644 --- a/packages/appkit/src/plugins/analytics/tests/metric.test.ts +++ b/packages/appkit/src/plugins/analytics/tests/metric.test.ts @@ -1704,6 +1704,45 @@ describe("metric — filter translator", () => { ).not.toThrow(); }); + test("rejects deep `or` even when paired with empty `and` (else-if bypass guard)", () => { + // Regression for the round-4 finding: the previous pre-check used + // `if (and) ... else if (or) ...` and walked only one branch. A + // payload of `{ and: [], or: }` slid past the + // empty-`and` walk and Zod's union recursion then stack-overflowed + // on the `or` branch. The pre-check now inspects BOTH keys. + let deepOr: any = { + member: "region", + operator: "equals", + values: ["EMEA"], + }; + for (let i = 0; i < 10_000; i += 1) { + deepOr = { or: [deepOr] }; + } + expect(() => + validateMetricRequest(REVENUE_PHASE3_REGISTRATION, { + measures: ["arr"], + filter: { and: [], or: [deepOr] } as any, + }), + ).toThrowError(/fields:.*filter/); + }); + + test("rejects breadth-DoS: a single group with too many children", () => { + // Without the breadth cap, `{ and: [...100k empty nodes...] }` would + // push 100k frames onto the iterative pre-check's stack — eventual + // OOM. Cap the per-group child count at the validation boundary. + const wide = Array.from({ length: 1000 }, () => ({ + member: "region", + operator: "equals" as const, + values: ["EMEA"], + })); + expect(() => + validateMetricRequest(REVENUE_PHASE3_REGISTRATION, { + measures: ["arr"], + filter: { and: wide }, + }), + ).toThrowError(/fields:.*filter/); + }); + test("rejects pathologically deep filter without stack-overflow (pre-parse cap)", () => { // Without the iterative pre-parse depth check, Zod's recursive parse // walks the union/object tree on the call stack BEFORE the validator's From 4b6d07f1c7444a30bf6e145af89730564dd960ea Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Mon, 4 May 2026 19:17:35 +0200 Subject: [PATCH 27/34] fix(appkit, appkit-ui): widen fail-closed gate; correct TextEncoder rationale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-ups from round 4: - Extend the metric-route fail-closed gate to trip on either empty `knownMeasures` OR empty `knownDimensions`. The round-1 gate only guarded the measure list; a metric whose DESCRIBE produced measures but zero dimensions still admitted arbitrary `dimensions` and `filter.member` identifiers, and those flow directly into the SQL `WHERE`/`GROUP BY` clauses. Same class of fall-open as round 1, just on the sibling field. Regression test: empty knownDimensions on a registration with non-empty knownMeasures must produce 503 METRIC_REGISTRY_NOT_READY. - Hoist `TextEncoder` to module scope and correct the misleading "constant-time" comment in `useMetricView` — `encode()` is O(bytes), same big-O as Blob's internal encoding; the win was avoiding the Blob wrapper allocation, not constant-time-ness. The shared encoder is stateless and safe to reuse. Co-authored-by: Isaac --- .../src/react/hooks/use-metric-view.ts | 15 +++++++-- .../appkit/src/plugins/analytics/analytics.ts | 23 +++++++++----- .../plugins/analytics/tests/metric.test.ts | 31 +++++++++++++++++++ 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/packages/appkit-ui/src/react/hooks/use-metric-view.ts b/packages/appkit-ui/src/react/hooks/use-metric-view.ts index 7b243fc43..7696dc4af 100644 --- a/packages/appkit-ui/src/react/hooks/use-metric-view.ts +++ b/packages/appkit-ui/src/react/hooks/use-metric-view.ts @@ -13,6 +13,13 @@ import type { UseMetricViewRow, } from "./types"; +/** + * Module-level singleton — `new TextEncoder()` is cheap but constructing + * one per byte-count call is still wasted allocation. The encoder is + * stateless, so a single shared instance is safe. + */ +const TEXT_ENCODER = new TextEncoder(); + /** * Subscribe to a metric-view query over SSE. * @@ -122,9 +129,11 @@ export function useMetricView< } const serialized = JSON.stringify(body); // Avoid the Blob allocation just to count bytes — it's a hot path - // on dashboards with many metric tiles. `TextEncoder` is constant- - // time per call and produces the same UTF-8 byte length. - const sizeInBytes = new TextEncoder().encode(serialized).length; + // on dashboards with many metric tiles. `TextEncoder.encode()` is + // O(n) over the serialized bytes (same big-O as Blob's internal + // encoding) but skips the Blob wrapper allocation. The encoder is + // hoisted to module scope so we don't allocate one per call either. + const sizeInBytes = TEXT_ENCODER.encode(serialized).length; if (sizeInBytes > maxParametersSize) { throw new Error( "useMetricView: Request body size exceeds the maximum allowed size", diff --git a/packages/appkit/src/plugins/analytics/analytics.ts b/packages/appkit/src/plugins/analytics/analytics.ts index 7df357f53..3b2d04922 100644 --- a/packages/appkit/src/plugins/analytics/analytics.ts +++ b/packages/appkit/src/plugins/analytics/analytics.ts @@ -301,17 +301,24 @@ export class AnalyticsPlugin extends Plugin { return; } - // Fail-closed: if the build-time DESCRIBE never produced a measure list - // for this metric, the body validator falls open (no allowlist) and the - // SQL constructor would let arbitrary measure/dim references through to - // the warehouse. Refuse the request so an empty/missing - // `metrics.metadata.json` cannot become a schema-enumeration vector. - // The clear server-side fix is to (re-)run `pnpm exec appkit metric sync`. - if (registration.knownMeasures.length === 0) { + // Fail-closed: if the build-time DESCRIBE never produced a measure or + // dimension list for this metric, the body validator falls open (no + // allowlist) and the SQL constructor would let arbitrary measure/dim + // references through to the warehouse — including `filter.member` + // identifiers that interpolate directly into the WHERE clause. Refuse + // the request so an empty/missing `metrics.metadata.json` cannot + // become a schema-enumeration vector for either field. The clear + // server-side fix is to (re-)run `pnpm exec appkit metric sync`. + if ( + registration.knownMeasures.length === 0 || + registration.knownDimensions.length === 0 + ) { logger.warn( req, - "Metric %s registered but build-time metadata is empty — refusing the request. Run `appkit metric sync` to populate metrics.metadata.json.", + "Metric %s registered but build-time metadata is empty (measures=%d, dimensions=%d) — refusing the request. Run `appkit metric sync` to populate metrics.metadata.json.", key, + registration.knownMeasures.length, + registration.knownDimensions.length, ); res.status(503).json({ error: "Metric registry not initialized", diff --git a/packages/appkit/src/plugins/analytics/tests/metric.test.ts b/packages/appkit/src/plugins/analytics/tests/metric.test.ts index 946cf0af0..43525e24f 100644 --- a/packages/appkit/src/plugins/analytics/tests/metric.test.ts +++ b/packages/appkit/src/plugins/analytics/tests/metric.test.ts @@ -918,6 +918,37 @@ describe("AnalyticsPlugin — metric route handler", () => { expect(errorPayload.error).toBe("Metric registry not initialized"); }); + test("returns 503 when knownDimensions is empty (fail-closed on either side)", async () => { + // The fail-closed gate must trip on EITHER empty measures OR empty + // dimensions. With non-empty measures but empty knownDimensions, the + // validator otherwise falls open on dimension identifiers (filter + // members, GROUP BY targets) and the SQL constructor interpolates them + // into the WHERE clause directly. + const plugin = new AnalyticsPlugin(config); + plugin._setMetricRegistryForTesting({ + revenue: { + ...REVENUE_REGISTRATION, + knownMeasures: ["arr"], + knownDimensions: [], + }, + }); + const { router, getHandler } = createMockRouter(); + plugin.injectRoutes(router); + + const handler = getHandler("POST", "/metric/:key"); + const mockReq = createMockRequest({ + params: { key: "revenue" }, + body: { measures: ["arr"] }, + }); + const mockRes = createMockResponse(); + + await handler(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(503); + const errorPayload = (mockRes.json as any).mock.calls[0][0]; + expect(errorPayload.code).toBe("METRIC_REGISTRY_NOT_READY"); + }); + test("returns 400 with the canonical error shape on validator failure", async () => { const plugin = new AnalyticsPlugin(config); plugin._setMetricRegistryForTesting({ From 66a48cb30bdfbc65726ace5fbd27a7e4ecfc01f9 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Mon, 4 May 2026 19:29:02 +0200 Subject: [PATCH 28/34] fix(appkit): support measure-only metric views, tighten dim/filter fall-open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-4 correctness review surfaced that the round-4 fail-closed gate widening I shipped in 4b6d07f1 broke a legitimate shape: a measure-only KPI metric registers with `knownDimensions: []`, and the gate then returned 503 for every request — including the request shapes that contract types declare as legal (`dimensions?` is optional). Two GPT runs reached opposing conclusions in r4: - Security: empty knownDimensions is a fall-open vector - Correctness: empty knownDimensions is a legitimate shape Both correct. The clean fix is to separate the two concerns: - Revert the route gate to `knownMeasures.length === 0` only. `knownMeasures` is unambiguous (every metric has at least one measure by definition), so an empty array always means metadata is missing. - Tighten the validator instead of the gate: when `knownDimensions` is empty, `dimensions` items are typed as `z.never()` and any filter predicate's `member` is rejected. Measure-only metrics still accept requests that omit dimensions/filter; the security fall-open path (where empty knownDimensions let arbitrary identifiers through to SQL) is now closed at validation time, not at the gate. Replaced the round-4 503 regression test with three new ones: KPI request accepted, dimensions rejected on measure-only, filter rejected on measure-only. The pre-existing "falls open on timeGrain when metadata is empty" test depended on dimensions ALSO falling open, so it's now unreachable — deleted with a comment explaining the new state. Co-authored-by: Isaac --- .../appkit/src/plugins/analytics/analytics.ts | 29 ++-- .../appkit/src/plugins/analytics/metric.ts | 24 +++- .../plugins/analytics/tests/metric.test.ts | 131 ++++++++++++++---- 3 files changed, 136 insertions(+), 48 deletions(-) diff --git a/packages/appkit/src/plugins/analytics/analytics.ts b/packages/appkit/src/plugins/analytics/analytics.ts index 3b2d04922..163e2a39d 100644 --- a/packages/appkit/src/plugins/analytics/analytics.ts +++ b/packages/appkit/src/plugins/analytics/analytics.ts @@ -301,24 +301,23 @@ export class AnalyticsPlugin extends Plugin { return; } - // Fail-closed: if the build-time DESCRIBE never produced a measure or - // dimension list for this metric, the body validator falls open (no - // allowlist) and the SQL constructor would let arbitrary measure/dim - // references through to the warehouse — including `filter.member` - // identifiers that interpolate directly into the WHERE clause. Refuse - // the request so an empty/missing `metrics.metadata.json` cannot - // become a schema-enumeration vector for either field. The clear - // server-side fix is to (re-)run `pnpm exec appkit metric sync`. - if ( - registration.knownMeasures.length === 0 || - registration.knownDimensions.length === 0 - ) { + // Fail-closed: if the build-time DESCRIBE never produced a measure list + // for this metric, the body validator falls open (no allowlist) and the + // SQL constructor would let arbitrary measure references through to + // the warehouse. Refuse the request so an empty/missing + // `metrics.metadata.json` cannot become a schema-enumeration vector. + // The clear server-side fix is to (re-)run `pnpm exec appkit metric sync`. + // + // We deliberately do NOT gate on `knownDimensions.length === 0` here — + // a measure-only KPI metric legitimately has zero dimensions and must + // continue to work. The validator-side tightening below rejects + // `dimensions` / `filter` payloads against an empty `knownDimensions`, + // which closes the fall-open path without blocking the legitimate case. + if (registration.knownMeasures.length === 0) { logger.warn( req, - "Metric %s registered but build-time metadata is empty (measures=%d, dimensions=%d) — refusing the request. Run `appkit metric sync` to populate metrics.metadata.json.", + "Metric %s registered but build-time metadata is empty — refusing the request. Run `appkit metric sync` to populate metrics.metadata.json.", key, - registration.knownMeasures.length, - registration.knownDimensions.length, ); res.status(503).json({ error: "Metric registry not initialized", diff --git a/packages/appkit/src/plugins/analytics/metric.ts b/packages/appkit/src/plugins/analytics/metric.ts index 4ee2c0d05..ac8970808 100644 --- a/packages/appkit/src/plugins/analytics/metric.ts +++ b/packages/appkit/src/plugins/analytics/metric.ts @@ -407,6 +407,12 @@ export function makeMetricRequestSchema( const baseDimensionSchema = z .string() .min(1, { message: "dimension name cannot be empty" }); + // When the metric has no registered dimensions (a measure-only KPI), + // `dimensions` must be empty/omitted: any non-empty entry is rejected. + // This closes the previous fall-open path (where empty `knownDimensions` + // skipped the allowlist refinement) without blocking the legitimate + // measure-only case — `dimensions: undefined` and `dimensions: []` still + // pass the surrounding `.optional()` and `.min(0)` shape. const dimensionItemSchema = knownDimensions.length > 0 ? baseDimensionSchema.refine( @@ -415,7 +421,7 @@ export function makeMetricRequestSchema( message: `dimension must be one of: ${knownDimensions.join(", ")}`, }, ) - : baseDimensionSchema; + : (z.never() as unknown as z.ZodType); // Aggregate the union of grains the metric view supports. Empty union means // no time-typed dimensions are declared — `timeGrain` cannot be set. @@ -646,10 +652,18 @@ function validateFilterTree( // the registry-aware constraints. const predicate = node as MetricPredicate; - if ( - registry.knownDimensions.length > 0 && - !registry.knownDimensions.includes(predicate.member) - ) { + if (registry.knownDimensions.length === 0) { + // The metric has no registered dimensions (measure-only KPI) — any + // filter predicate references a member that cannot exist. Reject + // rather than fall open. This complements the validator-level + // dimensionItemSchema tightening: filter and dimensions are both + // gated by the same registry signal. + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [...path, "member"], + message: `filter member "${predicate.member}" is not a declared dimension`, + }); + } else if (!registry.knownDimensions.includes(predicate.member)) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: [...path, "member"], diff --git a/packages/appkit/src/plugins/analytics/tests/metric.test.ts b/packages/appkit/src/plugins/analytics/tests/metric.test.ts index 43525e24f..bfc35d36d 100644 --- a/packages/appkit/src/plugins/analytics/tests/metric.test.ts +++ b/packages/appkit/src/plugins/analytics/tests/metric.test.ts @@ -222,7 +222,28 @@ describe("metric — pure helpers", () => { ).toThrowError(/dimensions\.0/); }); - test("falls open on dimensions when knownDimensions is empty", () => { + test("rejects non-empty `dimensions` when knownDimensions is empty (measure-only metric)", () => { + // Round-4 tightening: a measure-only metric registers + // `knownDimensions: []`, and any non-empty `dimensions` request + // must be rejected. The old fall-open behavior here was the + // round-4 security finding ("knownDimensions=[] fall-open" — empty + // registry let arbitrary dimension identifiers through to SQL). + const looseRegistration: MetricRegistration = { + ...REVENUE_REGISTRATION, + knownDimensions: [], + knownTimeGrainsByDim: {}, + }; + expect(() => + validateMetricRequest(looseRegistration, { + measures: ["arr"], + dimensions: ["any_column"], + }), + ).toThrowError(/fields:.*dimensions/); + }); + + test("accepts measure-only requests when knownDimensions is empty", () => { + // Complement of the above — a measure-only metric must still + // accept requests that simply omit dimensions and filter. const looseRegistration: MetricRegistration = { ...REVENUE_REGISTRATION, knownDimensions: [], @@ -230,9 +251,8 @@ describe("metric — pure helpers", () => { }; const parsed = validateMetricRequest(looseRegistration, { measures: ["arr"], - dimensions: ["any_column"], }); - expect(parsed.dimensions).toEqual(["any_column"]); + expect(parsed.dimensions).toBeUndefined(); }); // ── Phase 2: time grain ───────────────────────────────────────────── @@ -310,23 +330,14 @@ describe("metric — pure helpers", () => { ).toThrowError(); }); - test("falls open on timeGrain when metadata is empty (no metrics.metadata.json)", () => { - // Without build-time metadata the validator can't tell which dims are - // time-typed. Mirror the dimensions-fall-open behavior: accept the - // request and let the warehouse reject incompatible grains. - const noMetadataRegistration: MetricRegistration = { - ...REVENUE_REGISTRATION, - knownDimensions: [], - knownTimeGrainsByDim: {}, - }; - expect(() => - validateMetricRequest(noMetadataRegistration, { - measures: ["arr"], - dimensions: ["created_at"], - timeGrain: "month", - }), - ).not.toThrowError(); - }); + // Note: the previous "falls open on timeGrain when metadata is empty" + // test was deleted in round 4. Its premise depended on `dimensions` + // also falling open, which the round-4 validator tightening removed: + // a request with empty `knownDimensions` and a non-empty `dimensions` + // array now hits the rejection path before timeGrain is reached. + // Empty-metadata registrations are also blocked at the route's 503 + // fail-closed gate via `knownMeasures.length === 0`, so this code + // path is no longer reachable in practice. }); describe("buildMetricSql", () => { @@ -918,12 +929,10 @@ describe("AnalyticsPlugin — metric route handler", () => { expect(errorPayload.error).toBe("Metric registry not initialized"); }); - test("returns 503 when knownDimensions is empty (fail-closed on either side)", async () => { - // The fail-closed gate must trip on EITHER empty measures OR empty - // dimensions. With non-empty measures but empty knownDimensions, the - // validator otherwise falls open on dimension identifiers (filter - // members, GROUP BY targets) and the SQL constructor interpolates them - // into the WHERE clause directly. + test("accepts a measure-only request when knownDimensions is empty (KPI metric)", async () => { + // Measure-only metric views are a legitimate shape — the public + // contract declares `dimensions?: string[]`. The route must not 503 + // a request that omits dimensions just because the registry has none. const plugin = new AnalyticsPlugin(config); plugin._setMetricRegistryForTesting({ revenue: { @@ -932,6 +941,9 @@ describe("AnalyticsPlugin — metric route handler", () => { knownDimensions: [], }, }); + (plugin as any).SQLClient.executeStatement = vi + .fn() + .mockResolvedValue({ result: { data: [{ arr: 1234 }] } }); const { router, getHandler } = createMockRouter(); plugin.injectRoutes(router); @@ -943,10 +955,73 @@ describe("AnalyticsPlugin — metric route handler", () => { const mockRes = createMockResponse(); await handler(mockReq, mockRes); + expect(mockRes.status).not.toHaveBeenCalledWith(503); + }); - expect(mockRes.status).toHaveBeenCalledWith(503); + test("rejects a non-empty `dimensions` request against a measure-only metric", async () => { + // Closes the round-3 fall-open path: when knownDimensions is empty, + // the validator must reject any non-empty `dimensions` entry — those + // identifiers would otherwise flow into the SELECT/GROUP BY clauses + // of a metric view that has no dimensions registered. + const plugin = new AnalyticsPlugin(config); + plugin._setMetricRegistryForTesting({ + revenue: { + ...REVENUE_REGISTRATION, + knownMeasures: ["arr"], + knownDimensions: [], + }, + }); + const { router, getHandler } = createMockRouter(); + plugin.injectRoutes(router); + + const handler = getHandler("POST", "/metric/:key"); + const mockReq = createMockRequest({ + params: { key: "revenue" }, + body: { measures: ["arr"], dimensions: ["secret_col"] }, + }); + const mockRes = createMockResponse(); + + await handler(mockReq, mockRes); + expect(mockRes.status).toHaveBeenCalledWith(400); const errorPayload = (mockRes.json as any).mock.calls[0][0]; - expect(errorPayload.code).toBe("METRIC_REGISTRY_NOT_READY"); + expect(errorPayload.code).toBe("VALIDATION_ERROR"); + expect(errorPayload.error).toMatch(/fields:.*dimensions/); + }); + + test("rejects a `filter` request against a measure-only metric", async () => { + // Same fall-open closure as the dimensions case: filter members + // would otherwise interpolate into the WHERE clause for a metric + // that has no dimensions to filter on. + const plugin = new AnalyticsPlugin(config); + plugin._setMetricRegistryForTesting({ + revenue: { + ...REVENUE_REGISTRATION, + knownMeasures: ["arr"], + knownDimensions: [], + }, + }); + const { router, getHandler } = createMockRouter(); + plugin.injectRoutes(router); + + const handler = getHandler("POST", "/metric/:key"); + const mockReq = createMockRequest({ + params: { key: "revenue" }, + body: { + measures: ["arr"], + filter: { + member: "secret_col", + operator: "equals", + values: ["x"], + }, + }, + }); + const mockRes = createMockResponse(); + + await handler(mockReq, mockRes); + expect(mockRes.status).toHaveBeenCalledWith(400); + const errorPayload = (mockRes.json as any).mock.calls[0][0]; + expect(errorPayload.code).toBe("VALIDATION_ERROR"); + expect(errorPayload.error).toMatch(/fields:.*filter/); }); test("returns 400 with the canonical error shape on validator failure", async () => { From 614c212a214b571999a42e8ca4f1fa54c292ad0d Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Mon, 4 May 2026 20:04:34 +0200 Subject: [PATCH 29/34] fix(appkit): close timeGrain cache-bypass, AbortError mismatch, OBO key collision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three round-5 blockers landed in one go: - timeGrain z.never() when no time-typed dimensions are registered. Mirrors the round-4 dimensions tightening: the previous fall-open schema accepted any token; the SQL came out identical (no time-typed dim → no `date_trunc`), but `composeMetricCacheKey` salts the cache entry with the raw token. A hostile caller could vary `month/week/foo_bar/...` to force unbounded cache misses + warehouse re-execution. CWE-400 / CWE-20. - Re-check `signal.aborted` in the executeStream catch before scrubbing. The connector throws `ExecutionError.canceled()` (name "ExecutionError", message "Statement was canceled") on cancellation. The round-3 fast-path keyed on `name === "AbortError"` and missed it, the AppKitError 5xx scrub then masked the message, and stream-manager's substring fallback (`"operation was aborted"`) no longer matched — client cancellations surfaced as opaque UPSTREAM_ERROR. The `signal.aborted` re-check is class-agnostic: any error observed while the abort signal was already fired is a cancellation, regardless of the error's class or message. - OBO `deriveMetricExecutorKey` throws AuthenticationError on missing / whitespace-only identity instead of falling back to a shared "anonymous" sentinel. Two distinct misconfigured OBO callers were otherwise hashing to the same cache scope — a cross-user data leak. The route's existing try/catch wraps this call, so the throw lands on the canonical 401 envelope. Tests: measure-only timeGrain rejection; OBO empty-identity AuthenticationError (covering null/undefined/empty/whitespace). Co-authored-by: Isaac --- .../appkit/src/plugins/analytics/analytics.ts | 18 +++++- .../appkit/src/plugins/analytics/metric.ts | 34 +++++++---- .../plugins/analytics/tests/metric.test.ts | 57 +++++++++++-------- 3 files changed, 72 insertions(+), 37 deletions(-) diff --git a/packages/appkit/src/plugins/analytics/analytics.ts b/packages/appkit/src/plugins/analytics/analytics.ts index 163e2a39d..235fcaeaf 100644 --- a/packages/appkit/src/plugins/analytics/analytics.ts +++ b/packages/appkit/src/plugins/analytics/analytics.ts @@ -433,7 +433,23 @@ export class AnalyticsPlugin extends Plugin { // Cancellation must pass through unwrapped so the framework's // stream layer can distinguish client-driven aborts from real // failures (different telemetry, no error event emitted). - if (err instanceof Error && err.name === "AbortError") { + // + // Two signals indicate cancellation: + // + // 1. The error itself reports `AbortError` — what `fetch` / + // `AbortController` produce. + // 2. `signal.aborted` is true — what the AppKit SQL connector + // surfaces via `ExecutionError.canceled()` (name + // "ExecutionError", message "Statement was canceled"), which + // the round-3 `name === "AbortError"` check missed and the + // 5xx-scrub branch then masked further. Re-checking + // `signal.aborted` here catches that path: any error + // observed while the abort signal was already fired is a + // cancellation, regardless of the error's class or message. + if ( + signal?.aborted || + (err instanceof Error && err.name === "AbortError") + ) { throw err; } // Server-side scrub for the SSE error envelope. Without this, any diff --git a/packages/appkit/src/plugins/analytics/metric.ts b/packages/appkit/src/plugins/analytics/metric.ts index ac8970808..756ecfdab 100644 --- a/packages/appkit/src/plugins/analytics/metric.ts +++ b/packages/appkit/src/plugins/analytics/metric.ts @@ -3,7 +3,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { type SQLTypeMarker, sql as sqlHelpers } from "shared"; import { z } from "zod"; -import { ValidationError } from "../../errors"; +import { AuthenticationError, ValidationError } from "../../errors"; import { createLogger } from "../../logging/logger"; import type { IAnalyticsMetricRequest, @@ -425,6 +425,13 @@ export function makeMetricRequestSchema( // Aggregate the union of grains the metric view supports. Empty union means // no time-typed dimensions are declared — `timeGrain` cannot be set. + // Mirrors the dimensions/filter tightening: when the metric has no + // time-typed dimensions, `timeGrain` is rejected at validation time + // (typed as `z.never()`). The previous fall-open path silently accepted + // arbitrary grain tokens; the SQL came out identical (no time-typed dim + // → no `date_trunc` clause), but `composeMetricCacheKey` salts the + // cache entry with the raw token, so a hostile caller could vary + // `timeGrain` to force unbounded cache misses + warehouse re-execution. const grainsByDim = registration.knownTimeGrainsByDim; const allowedGrains = collectAllowedGrains(grainsByDim); const baseTimeGrainSchema = z @@ -435,7 +442,7 @@ export function makeMetricRequestSchema( ? baseTimeGrainSchema.refine((g: string) => allowedGrains.includes(g), { message: `timeGrain must be one of: ${allowedGrains.join(", ")}`, }) - : baseTimeGrainSchema; + : (z.never() as unknown as z.ZodType); // ── Filter sub-schema (Phase 3) ────────────────────────────────────────── // @@ -1539,10 +1546,12 @@ export function composeMetricCacheKey(input: { * what we need: same user → same key (so cache hits work), different users * → different keys (so isolation holds), and reverse lookup is infeasible. * - * For a missing or empty identity, falls back to a literal `"anonymous"` - * sentinel rather than an empty string. Empty-string hashes would collide - * across all callers without an identity — which is the bug a privacy-aware - * design must prevent. + * For OBO requests without a resolvable identity (missing or whitespace- + * only `x-forwarded-user`), throw `AuthenticationError.missingUserId()` + * rather than falling back to a shared `"anonymous"` sentinel — distinct + * misconfigured callers would otherwise share the same hash and read each + * other's cached results. The route's existing try/catch wraps this call, + * so the throw lands on the canonical 401 envelope. */ export function deriveMetricExecutorKey(input: { lane: MetricLane; @@ -1552,12 +1561,15 @@ export function deriveMetricExecutorKey(input: { return "sp"; } // OBO lane — hash the user identity so the raw email/principal never - // reaches the cache layer. `anonymous` is a sentinel for when the request - // has no resolvable identity (in practice this should not happen because - // OBO requires `x-forwarded-user`, but we belt-and-suspender it here). + // reaches the cache layer. Missing/whitespace identity is treated as a + // hard auth failure: the alternative ("anonymous" sentinel) collides + // every misconfigured caller into a single cache scope, so user A's + // results could leak to user B. const identity = input.userIdentity?.trim(); - const subject = identity && identity.length > 0 ? identity : "anonymous"; - return createHash("sha256").update(subject).digest("hex"); + if (!identity || identity.length === 0) { + throw AuthenticationError.missingUserId(); + } + return createHash("sha256").update(identity).digest("hex"); } /** diff --git a/packages/appkit/src/plugins/analytics/tests/metric.test.ts b/packages/appkit/src/plugins/analytics/tests/metric.test.ts index bfc35d36d..9916cb9ee 100644 --- a/packages/appkit/src/plugins/analytics/tests/metric.test.ts +++ b/packages/appkit/src/plugins/analytics/tests/metric.test.ts @@ -294,6 +294,27 @@ describe("metric — pure helpers", () => { ).toThrowError(/fields:.*timeGrain/); }); + test("rejects timeGrain on a measure-only metric (cache-bypass guard)", () => { + // Round-5 finding: a measure-only metric has empty + // knownTimeGrainsByDim, so allowedGrains is empty. The previous + // schema fell open and accepted any timeGrain string. The SQL came + // out identical (no time-typed dim → no date_trunc), but + // composeMetricCacheKey salts the cache key with the raw token — + // an attacker could vary `month/week/foo_bar/...` to force + // unbounded cache misses + warehouse re-execution. + const measureOnlyRegistration: MetricRegistration = { + ...REVENUE_REGISTRATION, + knownDimensions: [], + knownTimeGrainsByDim: {}, + }; + expect(() => + validateMetricRequest(measureOnlyRegistration, { + measures: ["arr"], + timeGrain: "month", + }), + ).toThrowError(/fields:.*timeGrain/); + }); + test("rejects timeGrain when metric has registered dims but none are time-typed", () => { // Tighter validation: when the registry knows the metric's dims but // none of them carry a time-grain set, `timeGrain` is meaningless on @@ -2211,31 +2232,17 @@ describe("metric — Phase 4 cache executor key", () => { expect(key).not.toContain("@"); }); - test("OBO-lane null identity falls back to anonymous sentinel", () => { - const a = deriveMetricExecutorKey({ lane: "obo", userIdentity: null }); - const b = deriveMetricExecutorKey({ - lane: "obo", - userIdentity: undefined, - }); - const c = deriveMetricExecutorKey({ lane: "obo", userIdentity: "" }); - const d = deriveMetricExecutorKey({ lane: "obo", userIdentity: " " }); - // All map to the same sentinel hash. - expect(a).toBe(b); - expect(b).toBe(c); - expect(c).toBe(d); - expect(a).toMatch(/^[0-9a-f]{64}$/); - }); - - test("OBO sentinel hash differs from any real identity hash", () => { - const sentinel = deriveMetricExecutorKey({ - lane: "obo", - userIdentity: undefined, - }); - const realUser = deriveMetricExecutorKey({ - lane: "obo", - userIdentity: "alice@example.com", - }); - expect(sentinel).not.toBe(realUser); + test("OBO-lane missing/whitespace identity throws AuthenticationError (no shared sentinel)", () => { + // Round-5 hardening: the previous "anonymous" sentinel let multiple + // misconfigured OBO callers share the same hashed cache scope — + // user A's results could leak to user B if both arrived with bad + // headers. Reject the request hard instead so missing identity + // fails fast on the canonical 401 path. + for (const userIdentity of [null, undefined, "", " "]) { + expect(() => + deriveMetricExecutorKey({ lane: "obo", userIdentity }), + ).toThrowError(/user/i); + } }); test("SP key differs from any OBO key (cross-lane isolation)", () => { From 7d297d948d2d86ed169db1985a1501cd1200a7c7 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Mon, 4 May 2026 21:00:26 +0200 Subject: [PATCH 30/34] fix(appkit): close signal.aborted race window; classify cancellations by error shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The round-5 `signal?.aborted` bypass I added in 614c212a created a security race: if `TimeoutInterceptor` aborted the signal during a warehouse error response (e.g., `TABLE_OR_VIEW_NOT_FOUND` carrying the FQN), the bypass rethrew the raw warehouse text unscrubbed and the framework's stream layer broadcast it to the still-connected client. Round-6 security review caught it (CWE-209 + CWE-362). Stop trusting `signal.aborted` as a generic bypass. Identify cancellations by error class/message instead — two narrow shapes: - real `AbortError` from fetch / `AbortController.abort()` - `ExecutionError` with the static message constructed by `ExecutionError.canceled()` ("Statement was canceled"). Match the message exactly so warehouse errors that happen to contain "canceled" cannot trick the bypass. Also fixes round-6 correctness: rethrowing `ExecutionError.canceled()` unchanged still classifies as UPSTREAM_ERROR in `StreamManager`, since its detection checks `name === "AbortError"` or `message.includes("operation was aborted")`. Normalize the connector's canceled variant to AbortError-shaped before rethrow so disconnects land on STREAM_ABORTED, not UPSTREAM_ERROR. Co-authored-by: Isaac --- .../appkit/src/plugins/analytics/analytics.ts | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/packages/appkit/src/plugins/analytics/analytics.ts b/packages/appkit/src/plugins/analytics/analytics.ts index 235fcaeaf..e8c14202c 100644 --- a/packages/appkit/src/plugins/analytics/analytics.ts +++ b/packages/appkit/src/plugins/analytics/analytics.ts @@ -430,27 +430,39 @@ export class AnalyticsPlugin extends Plugin { ); return { type: queryParameters.type, ...result }; } catch (err) { - // Cancellation must pass through unwrapped so the framework's - // stream layer can distinguish client-driven aborts from real - // failures (different telemetry, no error event emitted). + // Cancellation must pass through to the framework's stream layer + // SHAPED AS an AbortError so `StreamManager._categorizeError` + // classifies it as STREAM_ABORTED (it checks `name === "AbortError"` + // or `message.includes("operation was aborted")`). Re-throwing + // unchanged would otherwise let `ExecutionError.canceled()` fall + // through to UPSTREAM_ERROR. // - // Two signals indicate cancellation: + // We identify cancellations by error CLASS/MESSAGE rather than + // by `signal.aborted`. The signal-state check raced concurrent + // warehouse errors: if `TimeoutInterceptor` aborted the signal + // exactly when the warehouse returned `TABLE_OR_VIEW_NOT_FOUND`, + // the bypass would rethrow the raw warehouse text unscrubbed + // (round-6 security finding). Two narrow shapes: // - // 1. The error itself reports `AbortError` — what `fetch` / - // `AbortController` produce. - // 2. `signal.aborted` is true — what the AppKit SQL connector - // surfaces via `ExecutionError.canceled()` (name - // "ExecutionError", message "Statement was canceled"), which - // the round-3 `name === "AbortError"` check missed and the - // 5xx-scrub branch then masked further. Re-checking - // `signal.aborted` here catches that path: any error - // observed while the abort signal was already fired is a - // cancellation, regardless of the error's class or message. - if ( - signal?.aborted || - (err instanceof Error && err.name === "AbortError") - ) { - throw err; + // 1. real `AbortError` (`fetch` / `AbortController.abort()`) + // 2. `ExecutionError` with the static message constructed by + // `ExecutionError.canceled()` ("Statement was canceled") — + // the only ExecutionError variant that means cancellation. + // Match the message exactly so warehouse errors that happen + // to mention "canceled" cannot trick the bypass. + const isCancellation = + (err instanceof Error && err.name === "AbortError") || + (err instanceof ExecutionError && + err.message === "Statement was canceled"); + if (isCancellation) { + if (err instanceof Error && err.name === "AbortError") { + throw err; + } + // Normalize the connector's `ExecutionError.canceled()` to the + // AbortError shape `_categorizeError` recognizes. + const normalized = new Error("operation was aborted"); + normalized.name = "AbortError"; + throw normalized; } // Server-side scrub for the SSE error envelope. Without this, any // 4xx from the warehouse (e.g. TABLE_OR_VIEW_NOT_FOUND with a UC From 34dc0c537bde63b27c4ebfd74a6c0928526a45c8 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Mon, 4 May 2026 21:09:55 +0200 Subject: [PATCH 31/34] fix(appkit): trim x-forwarded-user; surface metric.json load failures on the route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-ups from the review-pr pass: - `resolveUserId()` and `asUser()` now `.trim()` the `x-forwarded-user` / `x-forwarded-access-token` headers before the truthiness check. A whitespace-only value previously passed through as a valid identity, so the cache layer collapsed distinct misconfigured callers into a shared scope. The round-5 fix in `deriveMetricExecutorKey` only caught this for metrics; tightening the shared helper covers all OBO paths (`.obo.sql` queries included). - `AnalyticsPlugin.setup()` now latches a `loadMetricRegistry()` failure on `metricRegistryLoadError` and `_handleMetricRoute` returns 503 `METRIC_REGISTRY_LOAD_FAILED` until it clears. The previous behavior — empty registry plus a warn log — turned a malformed `metric.json` into "every metric returns 404 Metric not found", which masked deployment errors. The 503 + distinct code give deployment pipelines + the route surface itself a clear signal. Other analytics routes (`/query/:key`, `/arrow-result`) stay available so a single-route configuration error doesn't break the whole plugin. Public message is generic; the parser-failure detail goes to `event.setContext()` only. Co-authored-by: Isaac --- packages/appkit/src/plugin/plugin.ts | 16 +++-- .../appkit/src/plugins/analytics/analytics.ts | 63 ++++++++++++++++--- .../plugins/analytics/tests/metric.test.ts | 34 ++++++++++ 3 files changed, 102 insertions(+), 11 deletions(-) diff --git a/packages/appkit/src/plugin/plugin.ts b/packages/appkit/src/plugin/plugin.ts index 75d994d88..f5f825431 100644 --- a/packages/appkit/src/plugin/plugin.ts +++ b/packages/appkit/src/plugin/plugin.ts @@ -323,8 +323,12 @@ export abstract class Plugin< * @throws AuthenticationError in production when no user header is present. */ protected resolveUserId(req: express.Request): string { - const userId = req.header("x-forwarded-user"); - if (userId) return userId; + // Trim before truthiness — a whitespace-only `x-forwarded-user` would + // otherwise pass through as a valid identity, and downstream consumers + // (cache key derivation, OBO proxy, telemetry) would treat distinct + // misconfigured callers as one. Reject it as a missing identity instead. + const userId = req.header("x-forwarded-user")?.trim(); + if (userId && userId.length > 0) return userId; if (process.env.NODE_ENV === "development") return getCurrentUserId(); throw AuthenticationError.missingToken( "Missing x-forwarded-user header. Cannot resolve user ID.", @@ -342,8 +346,12 @@ export abstract class Plugin< * In development mode (`NODE_ENV=development`), skips user impersonation instead of throwing. */ asUser(req: express.Request): this { - const token = req.header("x-forwarded-access-token"); - const userId = req.header("x-forwarded-user"); + const token = req.header("x-forwarded-access-token")?.trim(); + // Trim before truthiness — a whitespace-only header would otherwise + // pass downstream as a valid identity / token. The cache-key path + // would collapse distinct misconfigured callers into a shared scope, + // and the OBO proxy would attempt to authenticate with whitespace. + const userId = req.header("x-forwarded-user")?.trim(); const isDev = process.env.NODE_ENV === "development"; // In local development, skip user impersonation diff --git a/packages/appkit/src/plugins/analytics/analytics.ts b/packages/appkit/src/plugins/analytics/analytics.ts index e8c14202c..59a962d69 100644 --- a/packages/appkit/src/plugins/analytics/analytics.ts +++ b/packages/appkit/src/plugins/analytics/analytics.ts @@ -49,6 +49,18 @@ export class AnalyticsPlugin extends Plugin { */ private metricRegistry: Record = {}; + /** + * Latched error from the most recent `loadMetricRegistry()` attempt. + * `null` means the registry loaded cleanly (or `metric.json` was absent + * — also fine; metric views are an opt-in feature). When non-null, every + * `/metric/:key` request returns 503 with code `METRIC_REGISTRY_LOAD_FAILED` + * so deployment errors (malformed JSON, schema violations, missing + * required fields) surface as a clear server status rather than + * masquerading as 404s for every metric. Surfaces via the route only — + * the rest of the analytics plugin stays available. + */ + private metricRegistryLoadError: string | null = null; + constructor(config: IAnalyticsConfig) { super(config); this.config = config; @@ -61,19 +73,30 @@ export class AnalyticsPlugin extends Plugin { } /** - * Eagerly load the metric registry. Failures are logged at warn level (not - * thrown) so a malformed `metric.json` does not take down the whole app — - * the route handler returns a clean 404 for unregistered keys regardless. + * Eagerly load the metric registry. + * + * `setup()` does not throw — failures here would otherwise prevent the + * whole app (including unrelated plugins) from starting, which is too + * blunt for what is conceptually a single-route configuration error. + * Instead, latch the failure on `metricRegistryLoadError`: the metric + * route then returns 503 with a clear code so deployment pipelines + the + * /metric/:key surface itself reflect the broken state. Other analytics + * routes (`/query/:key`, `/arrow-result/:jobId`) continue to work. + * + * The previous behavior — empty `metricRegistry` plus a warn log — made + * malformed `metric.json` indistinguishable from missing keys (every + * metric returned 404 "Metric not found"), which masked deployment + * errors and matched a recurring review pattern across multiple rounds. */ async setup(): Promise { try { this.metricRegistry = await loadMetricRegistry(); + this.metricRegistryLoadError = null; } catch (err) { - logger.warn( - "Failed to load metric registry: %s", - err instanceof Error ? err.message : String(err), - ); + const reason = err instanceof Error ? err.message : String(err); + logger.warn("Failed to load metric registry: %s", reason); this.metricRegistry = {}; + this.metricRegistryLoadError = reason; } } @@ -289,6 +312,21 @@ export class AnalyticsPlugin extends Plugin { return; } + // Surface a startup-time registry-load failure on the route. Without + // this, a malformed metric.json would yield 404 for every key — which + // looks identical to "key never registered" and hides the deployment + // error. The full reason goes to telemetry only. + if (this.metricRegistryLoadError !== null) { + event?.setContext("analytics", { + metric_registry_load_error: this.metricRegistryLoadError, + }); + res.status(503).json({ + error: "Metric registry not available", + code: "METRIC_REGISTRY_LOAD_FAILED", + }); + return; + } + const registration = this.metricRegistry[key]; if (!registration) { // Don't echo the user-supplied `key` back in the public response. @@ -520,6 +558,17 @@ export class AnalyticsPlugin extends Plugin { this.metricRegistry = registry; } + /** + * Test-only seam: simulate a `loadMetricRegistry()` failure latched by + * `setup()`. Production code never calls this — `setup()` is the sole + * setter of `metricRegistryLoadError`. + * + * @internal + */ + _setMetricRegistryLoadErrorForTesting(reason: string | null): void { + this.metricRegistryLoadError = reason; + } + /** * Execute a SQL query using the current execution context. * diff --git a/packages/appkit/src/plugins/analytics/tests/metric.test.ts b/packages/appkit/src/plugins/analytics/tests/metric.test.ts index 9916cb9ee..be861bd3a 100644 --- a/packages/appkit/src/plugins/analytics/tests/metric.test.ts +++ b/packages/appkit/src/plugins/analytics/tests/metric.test.ts @@ -918,6 +918,40 @@ describe("AnalyticsPlugin — metric route handler", () => { expect(errorPayload.error).not.toMatch(/ghost/); }); + test("returns 503 when setup() failed to load metric.json (no silent 404)", async () => { + // Recurring review pattern: a malformed metric.json silently turned + // every metric request into 404 "Metric not found" because setup() + // swallowed the error and reset the registry to {}. That hid the + // deployment-config error from operators. Now a startup-time load + // failure is latched and surfaced on the route as 503 with a + // distinct code so deployment pipelines + the route reflect the + // broken state. The full reason stays in telemetry only. + const plugin = new AnalyticsPlugin(config); + plugin._setMetricRegistryForTesting({}); + plugin._setMetricRegistryLoadErrorForTesting( + "Invalid metric.json at /path: sp.0.source must be a three-part FQN", + ); + const { router, getHandler } = createMockRouter(); + plugin.injectRoutes(router); + + const handler = getHandler("POST", "/metric/:key"); + const mockReq = createMockRequest({ + params: { key: "revenue" }, + body: { measures: ["arr"] }, + }); + const mockRes = createMockResponse(); + + await handler(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(503); + const errorPayload = (mockRes.json as any).mock.calls[0][0]; + expect(errorPayload.code).toBe("METRIC_REGISTRY_LOAD_FAILED"); + // Public message must not echo the parser failure (which would + // include filesystem paths). The detail is logged via setContext. + expect(errorPayload.error).toBe("Metric registry not available"); + expect(errorPayload.error).not.toContain("metric.json"); + }); + test("returns 503 when the registered metric has no build-time metadata (fail-closed)", async () => { // Defense-in-depth: when `metrics.metadata.json` is missing or didn't // populate measures for this metric, the validator falls open and the From cc5639aa59cb189d8f288505cc6e9f8c788863e5 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Mon, 4 May 2026 22:31:29 +0200 Subject: [PATCH 32/34] fix(appkit): wait for DESCRIBE statement completion in metric-registry fetcher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `createWorkspaceDescribeFetcher` was calling `client.statementExecution.executeStatement(...)` without `wait_timeout`. On a cold (or recently-resumed) SQL warehouse, the API returns synchronously with `state: "PENDING"` and an empty `result.data_array` before the statement has actually run. `parseDescribeTableExtendedJson` reads that as "DESCRIBE TABLE EXTENDED returned no rows", marks the metric as a failure, and the build-time `metrics.metadata.json` ships with empty `measures` / `dimensions` for the affected metric. The runtime fail-closed gate (`knownMeasures.length === 0`) then 503s every metric request — even for views that exist in the warehouse. Add `wait_timeout: "30s"` to match the pattern in the SDK's own example (`examples/workspace/sql/execute-query.ts`). The API now blocks for up to 30 seconds for the statement to complete; once the warehouse is warm, DESCRIBE returns synchronously with populated `data_array`. `extractMetricColumns` then sees real columns and the build-time bundle ships with proper measure / dimension metadata. Symptom this resolves: dev-playground `/metrics` route showed "Could not load the revenue metric." for both demo metrics even though `appkit_demo.public.revenue_metrics` and `appkit_demo.public.customer_metrics` exist in the workspace and DESCRIBE-by-curl returns full schema. Co-authored-by: Isaac --- packages/appkit/src/type-generator/metric-registry.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/appkit/src/type-generator/metric-registry.ts b/packages/appkit/src/type-generator/metric-registry.ts index d60cee1fb..95a6773fe 100644 --- a/packages/appkit/src/type-generator/metric-registry.ts +++ b/packages/appkit/src/type-generator/metric-registry.ts @@ -926,6 +926,14 @@ export type DescribeFetcher = ( * Build a DescribeFetcher from a real WorkspaceClient + warehouseId. * * Kept narrow so it does not require importing the SDK at test time. + * + * `wait_timeout: "30s"` makes the API wait synchronously for the statement + * to complete (matching the SDK's own example pattern). Without an explicit + * wait, the call can return while the statement is still PENDING/RUNNING — + * the response carries no `data_array` yet, `parseDescribeTableExtendedJson` + * reads that as "returned no rows", and the registry ships empty. The + * runtime fail-closed gate then 503s every metric request, which is exactly + * the symptom we hit on a cold warehouse. */ export function createWorkspaceDescribeFetcher( warehouseId: string, @@ -935,6 +943,7 @@ export function createWorkspaceDescribeFetcher( const result = (await client.statementExecution.executeStatement({ statement: `DESCRIBE TABLE EXTENDED ${fqn} AS JSON`, warehouse_id: warehouseId, + wait_timeout: "30s", })) as DatabricksStatementExecutionResponse; return result; }; From d8a3bc3014ff39344fe4259227f6c12cadc2a9d4 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Mon, 4 May 2026 22:58:01 +0200 Subject: [PATCH 33/34] fix(appkit-ui): ignore late SSE events for an already-aborted controller MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useMetricView's `onError` callback bails silently when `abortController.signal.aborted` is true, but `onMessage` had no matching guard. Under React StrictMode's double-mount, the first mount's cleanup aborted controller A, but the server-side SSE writer still emitted a final `event: error` envelope on the already-open stream (cancellation hand-off). That envelope arrived at the hook's onMessage, sailed past the missing guard, hit the `parsed.type === "error"` branch, and surfaced a user-visible error before the second mount's data arrived. Production builds (StrictMode no-op) didn't see it; dev did. Pages with concurrent useMetricView subscriptions to the same metric (e.g. acmecorp-arr's /home with rollup + at-risk on the same view) amplified the visible window. Mirror the `onError` guard at the top of `onMessage`. Once the controller is aborted, drop ALL events from that stream uniformly (`result`, `arrow`, `error`) — the next mount/refetch creates a fresh controller and runs cleanly. Regression test simulates a late "error" envelope arriving after manual abort and asserts `error` stays `null`. Co-authored-by: Isaac --- .../hooks/__tests__/use-metric-view.test.ts | 33 +++++++++++++++++++ .../src/react/hooks/use-metric-view.ts | 9 +++++ 2 files changed, 42 insertions(+) diff --git a/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts index aab51b5cd..37c2412b5 100644 --- a/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts +++ b/packages/appkit-ui/src/react/hooks/__tests__/use-metric-view.test.ts @@ -350,6 +350,39 @@ describe("useMetricView", () => { expect(capturedCallbacks.signal?.aborted).toBe(true); }); + test("ignores late SSE events arriving after the controller was aborted", async () => { + // Regression: under React StrictMode the first mount's cleanup aborts + // the controller it owns, but the server-side SSE writer can still + // emit a final `event: error` envelope on the already-open stream + // (cancellation hand-off). Without an early `aborted` guard in + // onMessage, that envelope hit the error branch and surfaced a + // transient user-visible error before the second mount's data arrived. + // The fix mirrors the guard already present at the top of `onError`. + const { result } = renderHook(() => + useMetricView("revenue", { measures: ["arr"] }), + ); + + // Simulate the abort that StrictMode's cleanup phase performs. + const sig = capturedCallbacks.signal as + | (AbortSignal & { _override?: boolean }) + | undefined; + Object.defineProperty(sig, "aborted", { value: true, configurable: true }); + + // Now deliver a late error event for that aborted stream. + act(() => { + capturedCallbacks.onMessage?.({ + data: JSON.stringify({ + type: "error", + error: "Statement was canceled", + code: "EXECUTION_ERROR", + }), + }); + }); + + expect(result.current.error).toBeNull(); + expect(result.current.loading).toBe(true); + }); + test("does not refetch when re-rendered with structurally identical inline args", () => { const { rerender } = renderHook( ({ args }: { args: any }) => useMetricView("revenue", args), diff --git a/packages/appkit-ui/src/react/hooks/use-metric-view.ts b/packages/appkit-ui/src/react/hooks/use-metric-view.ts index 7696dc4af..039e23ae9 100644 --- a/packages/appkit-ui/src/react/hooks/use-metric-view.ts +++ b/packages/appkit-ui/src/react/hooks/use-metric-view.ts @@ -168,6 +168,15 @@ export function useMetricView< payload, signal: abortController.signal, onMessage: async (message) => { + // Bail silently if the controller we own is already aborted. Mirrors + // the guard at the top of `onError` below. Without this, the server- + // side SSE writer's final error envelope (emitted in response to our + // own cleanup-driven abort) lands on this handler, hits the + // `parsed.type === "error"` branch, and surfaces a transient error to + // the user — visible in dev under React StrictMode's double-mount, + // hidden in prod where StrictMode is a no-op. The next mount/refetch + // creates a fresh controller and runs cleanly. + if (abortController.signal.aborted) return; try { const parsed = JSON.parse(message.data); From a7bd698251e943a3f953d74ccbacbff87fe182a1 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Mon, 4 May 2026 22:59:05 +0200 Subject: [PATCH 34/34] chore(playground): refresh metric.d.ts + metrics.metadata.json from live DESCRIBE These artifacts had been generated against an unpopulated DESCRIBE response (the empty-`data_array` PENDING-state case fixed in cc5639aa). With `wait_timeout: "30s"` now in place, the build-time type-generator produces complete metadata for both demo metric views: - revenue: 4 measures (mrr, arr, new_arr, churned_arr), 3 dimensions (region, segment, created_at) with full time-grain enum on created_at. - customer_metrics: 3 measures (active_accounts, churn_rate, avg_ltv), 3 dimensions (segment, region, csm_email). Co-authored-by: Isaac --- .../shared/appkit-types/metric.d.ts | 68 +++++++++++++++++-- .../shared/appkit-types/metrics.metadata.json | 58 +++++++++++++--- 2 files changed, 111 insertions(+), 15 deletions(-) diff --git a/apps/dev-playground/shared/appkit-types/metric.d.ts b/apps/dev-playground/shared/appkit-types/metric.d.ts index 1cda6dc01..31e893cec 100644 --- a/apps/dev-playground/shared/appkit-types/metric.d.ts +++ b/apps/dev-playground/shared/appkit-types/metric.d.ts @@ -7,14 +7,66 @@ declare module "@databricks/appkit-ui/react" { key: "revenue"; source: "appkit_demo.public.revenue_metrics"; lane: "sp"; - measures: Record; - dimensions: Record; - measureKeys: never; - dimensionKeys: never; - timeGrains: never; + measures: { + /** @sqlType double */ + "mrr": number; + /** @sqlType double */ + "arr": number; + /** @sqlType double */ + "new_arr": number; + /** @sqlType double */ + "churned_arr": number; + }; + dimensions: { + /** @sqlType string */ + "region": string; + /** @sqlType string */ + "segment": string; + /** @sqlType timestamp_ltz @timeGrain day|hour|minute|month|quarter|week|year */ + "created_at": string; + }; + measureKeys: "mrr" | "arr" | "new_arr" | "churned_arr"; + dimensionKeys: "region" | "segment" | "created_at"; + timeGrains: "day" | "hour" | "minute" | "month" | "quarter" | "week" | "year"; metadata: { - measures: Record; - dimensions: Record; + measures: { + "mrr": { + type: "double"; + display_name: "Monthly Recurring Revenue"; + format: "$#,##0.00"; + }; + "arr": { + type: "double"; + display_name: "Annual Recurring Revenue"; + format: "$#,##0.00"; + description: "Annualized contract value across all active subscriptions"; + }; + "new_arr": { + type: "double"; + display_name: "New ARR"; + format: "$#,##0.00"; + }; + "churned_arr": { + type: "double"; + display_name: "Churned ARR"; + format: "$#,##0.00"; + }; + }; + dimensions: { + "region": { + type: "string"; + display_name: "Region"; + }; + "segment": { + type: "string"; + display_name: "Customer Segment"; + }; + "created_at": { + type: "timestamp_ltz"; + display_name: "Subscription Start"; + time_grain: readonly ["day", "hour", "minute", "month", "quarter", "week", "year"]; + }; + }; }; }; "customer_metrics": { @@ -45,6 +97,7 @@ declare module "@databricks/appkit-ui/react" { "active_accounts": { type: "bigint"; display_name: "Active Accounts"; + format: "#,##0"; }; "churn_rate": { type: "decimal"; @@ -53,6 +106,7 @@ declare module "@databricks/appkit-ui/react" { "avg_ltv": { type: "double"; display_name: "Average LTV"; + format: "$#,##0.00"; }; }; dimensions: { diff --git a/apps/dev-playground/shared/appkit-types/metrics.metadata.json b/apps/dev-playground/shared/appkit-types/metrics.metadata.json index 2933a738e..457ed04a5 100644 --- a/apps/dev-playground/shared/appkit-types/metrics.metadata.json +++ b/apps/dev-playground/shared/appkit-types/metrics.metadata.json @@ -1,11 +1,10 @@ { "customer_metrics": { - "source": "appkit_demo.public.customer_metrics", - "lane": "obo", "measures": { "active_accounts": { "type": "bigint", - "display_name": "Active Accounts" + "display_name": "Active Accounts", + "format": "#,##0" }, "churn_rate": { "type": "decimal", @@ -13,7 +12,8 @@ }, "avg_ltv": { "type": "double", - "display_name": "Average LTV" + "display_name": "Average LTV", + "format": "$#,##0.00" } }, "dimensions": { @@ -32,9 +32,51 @@ } }, "revenue": { - "source": "appkit_demo.public.revenue_metrics", - "lane": "sp", - "measures": {}, - "dimensions": {} + "measures": { + "mrr": { + "type": "double", + "display_name": "Monthly Recurring Revenue", + "format": "$#,##0.00" + }, + "arr": { + "type": "double", + "display_name": "Annual Recurring Revenue", + "format": "$#,##0.00", + "description": "Annualized contract value across all active subscriptions" + }, + "new_arr": { + "type": "double", + "display_name": "New ARR", + "format": "$#,##0.00" + }, + "churned_arr": { + "type": "double", + "display_name": "Churned ARR", + "format": "$#,##0.00" + } + }, + "dimensions": { + "region": { + "type": "string", + "display_name": "Region" + }, + "segment": { + "type": "string", + "display_name": "Customer Segment" + }, + "created_at": { + "type": "timestamp_ltz", + "display_name": "Subscription Start", + "time_grain": [ + "day", + "hour", + "minute", + "month", + "quarter", + "week", + "year" + ] + } + } } }
    +
    {formatLabel("segment", metadata?.dimensions.segment)} + {formatLabel( "active_accounts", metadata?.measures.active_accounts, )} + {formatLabel("churn_rate", metadata?.measures.churn_rate)}
    {row.segment} +
    {row.segment} {formatValue( row.active_accounts, metadata?.measures.active_accounts.format, )} + {formatValue( row.churn_rate, metadata?.measures.churn_rate.format, diff --git a/packages/appkit/src/connectors/sql-warehouse/client.ts b/packages/appkit/src/connectors/sql-warehouse/client.ts index d0a1c1816..ce52a93dc 100644 --- a/packages/appkit/src/connectors/sql-warehouse/client.ts +++ b/packages/appkit/src/connectors/sql-warehouse/client.ts @@ -243,6 +243,15 @@ export class SQLWarehouseConnector { ); } + // Preserve native AbortError identity. Without this, the wrap below + // overwrites `name` (to "ExecutionError") and the downstream + // stream-manager._categorizeError can no longer distinguish a + // legitimate client cancellation from a real upstream failure — + // the duck-type `statusCode` fallback would route every aborted + // SQL through SSEErrorCode.UPSTREAM_ERROR. + if (error instanceof Error && error.name === "AbortError") { + throw error; + } if (error instanceof AppKitError) { throw error; } @@ -383,6 +392,11 @@ export class SQLWarehouseConnector { }); // error logging is handled by executeStatement's catch block (gated on isAborted) + if (error instanceof Error && error.name === "AbortError") { + // Preserve AbortError identity for stream-manager classification — + // see executeStatement's catch for the rationale. + throw error; + } if (error instanceof AppKitError) { throw error; } diff --git a/packages/appkit/src/stream/stream-manager.ts b/packages/appkit/src/stream/stream-manager.ts index 6a1a70d06..d042e35ba 100644 --- a/packages/appkit/src/stream/stream-manager.ts +++ b/packages/appkit/src/stream/stream-manager.ts @@ -400,6 +400,19 @@ export class StreamManager { return SSEErrorCode.STREAM_ABORTED; } + // Defense-in-depth: when an upstream layer (e.g., SQL warehouse client) + // wraps an AbortError into a domain error, the original `name` is lost + // but the message survives. Detect aborts via message substring before + // falling through to the statusCode-based UPSTREAM_ERROR classification — + // otherwise legitimate client cancellations get logged at error level + // and surfaced to consumers as if the warehouse failed. + if ( + message.includes("operation was aborted") || + message.includes("the request was aborted") + ) { + return SSEErrorCode.STREAM_ABORTED; + } + // Detect upstream API errors (e.g., from Databricks SDK ApiError) if ( "statusCode" in error && diff --git a/packages/appkit/src/stream/tests/stream.test.ts b/packages/appkit/src/stream/tests/stream.test.ts index a10e1edc4..9da079cc0 100644 --- a/packages/appkit/src/stream/tests/stream.test.ts +++ b/packages/appkit/src/stream/tests/stream.test.ts @@ -346,6 +346,96 @@ describe("StreamManager", () => { }); }); + describe("error categorization", () => { + // Helper: capture the SSE error code emitted by streaming a generator + // that throws the supplied error. + async function captureCategorizedCode( + manager: StreamManager, + thrown: unknown, + ): Promise { + const { mockRes, events } = createMockResponse(); + async function* generator() { + yield { type: "start" }; + throw thrown; + } + await manager.stream(mockRes as any, generator); + const errorEvent = events.find((e) => e.startsWith("event: error")); + if (!errorEvent) return undefined; + const dataLine = events + .find((e) => e.startsWith("data:") && e.includes('"code"')) + ?.replace(/^data: /, "") + .replace(/\n\n$/, ""); + if (!dataLine) return undefined; + const parsed = JSON.parse(dataLine); + return parsed.code as string; + } + + test("classifies native AbortError as STREAM_ABORTED", async () => { + const err = new Error("operation aborted"); + err.name = "AbortError"; + const code = await captureCategorizedCode(streamManager, err); + expect(code).toBe("STREAM_ABORTED"); + }); + + test("classifies wrapped AbortError (whose name was overwritten) as STREAM_ABORTED", async () => { + // Simulates the SQL warehouse client's wrap behavior: an AbortError + // gets re-wrapped as ExecutionError, losing `name === "AbortError"` — + // but the message survives. The classifier MUST detect this via the + // message substring fallback. + class FakeExecutionError extends Error { + statusCode = 500; + constructor() { + super("Statement failed: The operation was aborted."); + this.name = "ExecutionError"; + } + } + const code = await captureCategorizedCode( + streamManager, + new FakeExecutionError(), + ); + expect(code).toBe("STREAM_ABORTED"); + }); + + test("classifies real upstream API errors (statusCode + non-abort message) as UPSTREAM_ERROR", async () => { + class FakeApiError extends Error { + statusCode = 503; + constructor() { + super("Statement failed: Internal warehouse error"); + this.name = "ExecutionError"; + } + } + const code = await captureCategorizedCode( + streamManager, + new FakeApiError(), + ); + expect(code).toBe("UPSTREAM_ERROR"); + }); + + test("classifies timeouts as TIMEOUT", async () => { + const code = await captureCategorizedCode( + streamManager, + new Error("Request timed out"), + ); + expect(code).toBe("TIMEOUT"); + }); + + test("classifies ECONNREFUSED / unavailable as TEMPORARY_UNAVAILABLE", async () => { + const code = await captureCategorizedCode( + streamManager, + new Error("connect ECONNREFUSED 127.0.0.1:443"), + ); + expect(code).toBe("TEMPORARY_UNAVAILABLE"); + }); + + test("falls through to INTERNAL_ERROR for opaque errors", async () => { + const code = await captureCategorizedCode( + streamManager, + new Error("something unexpected"), + ); + expect(code).toBe("INTERNAL_ERROR"); + }); + }); + describe("heartbeat", () => { test("should send heartbeat messages periodically", async () => { vi.useFakeTimers(); From 84534a78d3d1c1062ebf14f49ff6a1e85d67b537 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Thu, 30 Apr 2026 10:14:04 +0200 Subject: [PATCH 12/34] fix(appkit): infer time-grain set from SQL type, not from a non-existent YAML attribute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 originally read a `time_grain` attribute on each column in the DESCRIBE TABLE EXTENDED ... AS JSON response, with a "tolerant parser" accepting list / metadata.time_grain / { grains: [...] } shapes. The field does not exist in the UC metric-view schema: - Rust serde at universe/reyden/metric-view-serde/src/v11/column.rs enumerates the 7 known column properties: window, expr, format, display_name, name, comment, synonyms. - CREATE rejects unknown fields with "Unrecognized field 'time_grain' (class com.databricks.sql.serde.v11.Column), not marked as ignorable". - Grep for "time_grain" / "TimeGrain" across the entire serde tree (Rust port + Scala source under sql/catalyst/.../serde/) returns zero hits. So the previous implementation was reading a phantom field — when fed real DESCRIBE output, every dimension's `timeGrains` came back empty, which surfaces as `timeGrains: never` in the generated metric.d.ts and causes the server validator to reject `timeGrain: "month"` requests. Fix: replace `extractTimeGrains(obj)` (YAML-attribute lookup) with `inferTimeGrains(type)` (SQL-type-based inference): TIMESTAMP / TIMESTAMP_LTZ / TIMESTAMP_NTZ → 7 grains (minute..year) DATE → 5 grains (day..year, no sub-day) everything else → undefined (not time-typed) Inference is gated on `isMeasure: false` — measures aren't grouped on even when they resolve to a temporal type. Type matching is case-insensitive and strips parameterized suffixes (TIMESTAMP(6)). Updates 6 broken extractMetricColumns tests (which passed `time_grain: [...]` fixtures testing imaginary behavior) to type-driven equivalents, plus 5 downstream tests that fed `time_grain` into syncMetrics / buildMetricsMetadataBundle / generateMetricTypeDeclarations / generateMetricsMetadataJson fixtures. Snapshots regenerated. 41/41 type-gen tests pass; full backpressure (build, docs, check:fix, typecheck, test, knip) green. Co-authored-by: Isaac Signed-off-by: Atila Fassina --- .../shared/appkit-types/metric.d.ts | 5 +- .../shared/appkit-types/metrics.metadata.json | 11 +- .../src/type-generator/metric-registry.ts | 85 ++++---- .../metric-registry.test.ts.snap | 11 +- .../tests/metric-registry.test.ts | 196 +++++++++++------- 5 files changed, 178 insertions(+), 130 deletions(-) diff --git a/apps/dev-playground/shared/appkit-types/metric.d.ts b/apps/dev-playground/shared/appkit-types/metric.d.ts index e9f401f08..d07e5d839 100644 --- a/apps/dev-playground/shared/appkit-types/metric.d.ts +++ b/apps/dev-playground/shared/appkit-types/metric.d.ts @@ -22,12 +22,12 @@ declare module "@databricks/appkit-ui/react" { "region": string; /** @sqlType string */ "segment": string; - /** @sqlType timestamp_ltz */ + /** @sqlType timestamp_ltz @timeGrain day|hour|minute|month|quarter|week|year */ "created_at": string; }; measureKeys: "mrr" | "arr" | "new_arr" | "churned_arr"; dimensionKeys: "region" | "segment" | "created_at"; - timeGrains: never; + timeGrains: "day" | "hour" | "minute" | "month" | "quarter" | "week" | "year"; metadata: { measures: { "mrr": { @@ -60,6 +60,7 @@ declare module "@databricks/appkit-ui/react" { "created_at": { type: "timestamp_ltz"; display_name: "Subscription Start"; + time_grain: readonly ["day", "hour", "minute", "month", "quarter", "week", "year"]; }; }; }; diff --git a/apps/dev-playground/shared/appkit-types/metrics.metadata.json b/apps/dev-playground/shared/appkit-types/metrics.metadata.json index dfa1586ad..338133624 100644 --- a/apps/dev-playground/shared/appkit-types/metrics.metadata.json +++ b/apps/dev-playground/shared/appkit-types/metrics.metadata.json @@ -64,7 +64,16 @@ }, "created_at": { "type": "timestamp_ltz", - "display_name": "Subscription Start" + "display_name": "Subscription Start", + "time_grain": [ + "day", + "hour", + "minute", + "month", + "quarter", + "week", + "year" + ] } } } diff --git a/packages/appkit/src/type-generator/metric-registry.ts b/packages/appkit/src/type-generator/metric-registry.ts index 6813a9e05..f9bf81b9b 100644 --- a/packages/appkit/src/type-generator/metric-registry.ts +++ b/packages/appkit/src/type-generator/metric-registry.ts @@ -56,8 +56,9 @@ interface ResolvedMetricEntry { * Per-column metadata extracted from DESCRIBE TABLE EXTENDED ... AS JSON. * * Phase 1 captured measure flags + types. Phase 2 widens to time-typed - * dimensions: a column is "time-typed" iff its DESCRIBE entry carries a - * non-empty `time_grain` attribute listing the allowed grains for that column. + * dimensions: grain qualification is inferred from the column's SQL type + * (TIMESTAMP* / DATE) — the UC metric-view YAML schema has no per-column + * `time_grain` attribute, so the type is the only signal available. * * Phase 5 captures the YAML 1.1 semantic-metadata fields so the build-time * artifact is a complete record of what the metric view declares: display name @@ -88,10 +89,9 @@ export interface MetricColumnMetadata { */ format?: string; /** - * Allowed time-grains for this column when present in the YAML's `time_grain` - * attribute. Undefined means the column is not time-typed. An empty array is - * never produced — if the attribute is present but empty we treat the - * column as a regular dimension (matches "explicit only" semantics from the PRD). + * Standard time-grain set for this column, inferred from the SQL data type: + * TIMESTAMP* → 7 grains (minute..year); DATE → 5 grains (day..year). + * Undefined means the column is not time-typed. Measures never get grains. */ timeGrains?: string[]; } @@ -359,7 +359,14 @@ export function extractMetricColumns(parsed: unknown): MetricColumnMetadata[] { ]); const format = extractStringFromAny(obj, ["format", "format_spec"]); - const timeGrains = extractTimeGrains(obj); + // Time-grain inference is type-driven, not YAML-attribute-driven. + // Earlier versions of this code looked for a `time_grain` field on each + // column, but that field does not exist in UC's metric-view schema — + // the Rust serde at universe/reyden/metric-view-serde/src/v11/column.rs + // enumerates the 7 known column properties (window, expr, format, + // display_name, name, comment, synonyms). CREATE rejects `time_grain` + // with "Unrecognized field". Measures don't get grouped, so skip them. + const timeGrains = isMeasure ? undefined : inferTimeGrains(type); columns.push({ name, @@ -404,45 +411,37 @@ function extractStringFromAny( } /** - * Pull the allowed time-grain list for a column from the DESCRIBE entry. + * Infer the standard set of valid time grains for a dimension based on its + * SQL data type. * - * Time-grain may live at: - * 1. `time_grain: ["day", "week", "month"]` — the YAML 1.1 canonical form. - * 2. `metadata.time_grain: [...]` — when DESCRIBE wraps it under `metadata`. - * 3. `time_grain: { grains: [...] }` — defensive against future shape drift. + * TIMESTAMP / TIMESTAMP_LTZ / TIMESTAMP_NTZ → all 7 standard grains + * DATE → [day, week, month, quarter, year] (no sub-day grains) + * anything else → undefined (not time-typed) * - * Returns `undefined` for "not a time-typed column" (no attribute present, or - * attribute present but empty/malformed). The caller treats undefined-grains - * dimensions as regular dimensions. + * Earlier code looked for a `time_grain` attribute on the YAML column. That + * field does not exist in the UC metric-view schema (see the v11 Rust serde + * — Column has 7 known properties: window, expr, format, display_name, + * name, comment, synonyms; CREATE fails with "Unrecognized field + * 'time_grain'"). So grain qualification has to come from the column's + * resolved SQL type instead. */ -function extractTimeGrains(obj: Record): string[] | undefined { - let raw: unknown = obj.time_grain; - if (raw == null && obj.metadata && typeof obj.metadata === "object") { - raw = (obj.metadata as Record).time_grain; - } - if (raw == null) return undefined; - +function inferTimeGrains(type: string): string[] | undefined { + // Strip parameterized suffixes ("TIMESTAMP(6)" → "TIMESTAMP") and trim. + const normalized = type + .toLowerCase() + .replace(/\(.*\)$/, "") + .trim(); if ( - raw && - typeof raw === "object" && - !Array.isArray(raw) && - Array.isArray((raw as Record).grains) + normalized === "timestamp" || + normalized === "timestamp_ltz" || + normalized === "timestamp_ntz" ) { - raw = (raw as Record).grains; + return ["day", "hour", "minute", "month", "quarter", "week", "year"]; } - - if (!Array.isArray(raw)) return undefined; - const grains: string[] = []; - for (const g of raw) { - if (typeof g === "string" && g.trim().length > 0) { - grains.push(g.toLowerCase().trim()); - } + if (normalized === "date") { + return ["day", "month", "quarter", "week", "year"]; } - // Empty list → treat as not time-typed (defensive). Phase 2's contract is - // "time_grain populated" → time dimension. - if (grains.length === 0) return undefined; - // Stable order so the generated d.ts and metadata are deterministic. - return [...new Set(grains)].sort(); + return undefined; } /** @@ -673,7 +672,7 @@ interface MetricColumnSemanticMetadata { display_name?: string; format?: string; description?: string; - /** Only emitted on dimension entries where the YAML declared a non-empty `time_grain`. */ + /** Only emitted on dimension entries that resolved to a TIMESTAMP* or DATE SQL type (grain set inferred from type). */ time_grain?: readonly string[]; } @@ -750,9 +749,9 @@ export function buildMetricsMetadataBundle( * `time_grain`) and absent fields are simply not included, so the snapshot * diff is always minimal — consumers receive only what the YAML declared. * - * `time_grain` is only emitted on dimensions (the YAML 1.1 spec restricts it - * to dimension columns). Defends against DESCRIBE leaking a stray attribute - * onto a measure. + * `time_grain` is only emitted on dimensions whose SQL type is TIMESTAMP* or + * DATE — measures never receive a grain since they aren't grouped on. The + * caller (extractMetricColumns) skips inference for `isMeasure: true` columns. */ function buildColumnMetadata( col: MetricColumnMetadata, diff --git a/packages/appkit/src/type-generator/tests/__snapshots__/metric-registry.test.ts.snap b/packages/appkit/src/type-generator/tests/__snapshots__/metric-registry.test.ts.snap index 2f37a1629..c60fc988f 100644 --- a/packages/appkit/src/type-generator/tests/__snapshots__/metric-registry.test.ts.snap +++ b/packages/appkit/src/type-generator/tests/__snapshots__/metric-registry.test.ts.snap @@ -15,7 +15,7 @@ declare module "@databricks/appkit-ui/react" { "arr": number; }; dimensions: { - /** @sqlType TIMESTAMP @timeGrain day|month|week */ + /** @sqlType TIMESTAMP @timeGrain day|hour|minute|month|quarter|week|year */ "created_at": string; /** @sqlType STRING */ "region": string; @@ -24,7 +24,7 @@ declare module "@databricks/appkit-ui/react" { }; measureKeys: "arr"; dimensionKeys: "created_at" | "region" | "segment"; - timeGrains: "day" | "month" | "week"; + timeGrains: "day" | "hour" | "minute" | "month" | "quarter" | "week" | "year"; metadata: { measures: { "arr": { @@ -35,7 +35,7 @@ declare module "@databricks/appkit-ui/react" { dimensions: { "created_at": { type: "TIMESTAMP"; - time_grain: readonly ["day", "month", "week"]; + time_grain: readonly ["day", "hour", "minute", "month", "quarter", "week", "year"]; }; "region": { type: "STRING"; @@ -157,9 +157,12 @@ exports[`generateMetricsMetadataJson — snapshot > serializes a representative "display_name": "Period", "time_grain": [ "day", + "hour", + "minute", "month", "quarter", - "week" + "week", + "year" ] } } diff --git a/packages/appkit/src/type-generator/tests/metric-registry.test.ts b/packages/appkit/src/type-generator/tests/metric-registry.test.ts index 9e5514376..bb354b435 100644 --- a/packages/appkit/src/type-generator/tests/metric-registry.test.ts +++ b/packages/appkit/src/type-generator/tests/metric-registry.test.ts @@ -211,77 +211,106 @@ describe("extractMetricColumns", () => { }); // ── Phase 2: time-typed dimensions ──────────────────────────────────── - test("captures time_grain attribute on a time-typed dimension", () => { + test("infers all 7 standard grains for a TIMESTAMP dimension", () => { const cols = extractMetricColumns({ columns: [ - { - name: "created_at", - type: "DATE", - is_measure: false, - time_grain: ["day", "week", "month"], - }, + { name: "created_at", type: "TIMESTAMP", is_measure: false }, { name: "region", type: "STRING", is_measure: false }, ], }); expect(cols).toHaveLength(2); expect(cols[0]).toMatchObject({ name: "created_at", - type: "DATE", isMeasure: false, - timeGrains: ["day", "month", "week"], // sorted, deduped + timeGrains: ["day", "hour", "minute", "month", "quarter", "week", "year"], }); // Non-time dim has no timeGrains key. expect(cols[1].timeGrains).toBeUndefined(); }); - test("normalizes time_grain values to lowercase + sorted + deduped", () => { + test("infers 5 standard grains (no sub-day) for a DATE dimension", () => { const cols = extractMetricColumns({ - columns: [ - { - name: "ts", - type: "TIMESTAMP", - is_measure: false, - time_grain: ["MONTH", "day", "Day", "week"], - }, - ], + columns: [{ name: "billing_date", type: "DATE", is_measure: false }], }); - expect(cols[0].timeGrains).toEqual(["day", "month", "week"]); + expect(cols[0].timeGrains).toEqual([ + "day", + "month", + "quarter", + "week", + "year", + ]); }); - test("falls back to metadata.time_grain (DESCRIBE wraps it under metadata)", () => { + test("recognizes TIMESTAMP_LTZ and TIMESTAMP_NTZ aliases", () => { const cols = extractMetricColumns({ columns: [ - { - name: "ts", - type: "TIMESTAMP", - metadata: { is_measure: false, time_grain: ["day"] }, - }, + { name: "ts_ltz", type: "TIMESTAMP_LTZ", is_measure: false }, + { name: "ts_ntz", type: "TIMESTAMP_NTZ", is_measure: false }, ], }); - expect(cols[0].timeGrains).toEqual(["day"]); + expect(cols[0].timeGrains).toEqual([ + "day", + "hour", + "minute", + "month", + "quarter", + "week", + "year", + ]); + expect(cols[1].timeGrains).toEqual([ + "day", + "hour", + "minute", + "month", + "quarter", + "week", + "year", + ]); }); - test("treats empty time_grain attribute as not time-typed", () => { + test("type matching is case-insensitive", () => { const cols = extractMetricColumns({ columns: [ - { name: "ts", type: "TIMESTAMP", is_measure: false, time_grain: [] }, + { name: "a", type: "timestamp", is_measure: false }, + { name: "b", type: "Timestamp", is_measure: false }, + { name: "c", type: "DATE", is_measure: false }, + { name: "d", type: "date", is_measure: false }, ], }); - expect(cols[0].timeGrains).toBeUndefined(); + expect(cols[0].timeGrains?.length).toBe(7); + expect(cols[1].timeGrains?.length).toBe(7); + expect(cols[2].timeGrains?.length).toBe(5); + expect(cols[3].timeGrains?.length).toBe(5); + }); + + test("strips parameterized type suffixes like TIMESTAMP(6)", () => { + const cols = extractMetricColumns({ + columns: [{ name: "ts", type: "TIMESTAMP(6)", is_measure: false }], + }); + expect(cols[0].timeGrains?.length).toBe(7); }); - test("ignores non-string time_grain entries", () => { + test("does not infer grains for non-temporal types", () => { const cols = extractMetricColumns({ columns: [ - { - name: "ts", - type: "TIMESTAMP", - is_measure: false, - time_grain: ["day", null, 42, "week"], - }, + { name: "id", type: "BIGINT", is_measure: false }, + { name: "name", type: "STRING", is_measure: false }, + { name: "amount", type: "DECIMAL(38,2)", is_measure: false }, ], }); - expect(cols[0].timeGrains).toEqual(["day", "week"]); + for (const col of cols) { + expect(col.timeGrains).toBeUndefined(); + } + }); + + test("does not infer grains on measures even if their type is TIMESTAMP", () => { + // Measures are aggregated, never grouped on — grain inference is + // dimension-only. Defends against an unusual MEASURE() expression + // resolving to a temporal type. + const cols = extractMetricColumns({ + columns: [{ name: "last_event_at", type: "TIMESTAMP", is_measure: true }], + }); + expect(cols[0].timeGrains).toBeUndefined(); }); }); @@ -376,12 +405,7 @@ describe("generateMetricTypeDeclarations — snapshot", () => { is_measure: true, comment: "Annual recurring revenue", }, - { - name: "created_at", - type: "TIMESTAMP", - is_measure: false, - time_grain: ["day", "week", "month"], - }, + { name: "created_at", type: "TIMESTAMP", is_measure: false }, { name: "region", type: "STRING", is_measure: false }, { name: "segment", type: "STRING", is_measure: false }, ], @@ -392,9 +416,13 @@ describe("generateMetricTypeDeclarations — snapshot", () => { expect(output).toMatchSnapshot(); // Sanity assertions in addition to the snapshot, so future drift surfaces - // even when snapshots are blindly updated. - expect(output).toContain('timeGrains: "day" | "month" | "week"'); - expect(output).toContain("@timeGrain day|month|week"); + // even when snapshots are blindly updated. TIMESTAMP → all 7 standard grains. + expect(output).toContain( + 'timeGrains: "day" | "hour" | "minute" | "month" | "quarter" | "week" | "year"', + ); + expect(output).toContain( + "@timeGrain day|hour|minute|month|quarter|week|year", + ); expect(output).toContain('"created_at": string'); expect(output).toContain('"region": string'); }); @@ -542,12 +570,7 @@ describe("buildMetricsMetadataBundle", () => { comment: "ARR for the period", }, { name: "region", type: "STRING", is_measure: false }, - { - name: "created_at", - type: "TIMESTAMP", - is_measure: false, - time_grain: ["day", "month"], - }, + { name: "created_at", type: "TIMESTAMP", is_measure: false }, ], }); @@ -571,7 +594,15 @@ describe("buildMetricsMetadataBundle", () => { }, created_at: { type: "TIMESTAMP", - time_grain: ["day", "month"], + time_grain: [ + "day", + "hour", + "minute", + "month", + "quarter", + "week", + "year", + ], }, }, }); @@ -623,28 +654,27 @@ describe("buildMetricsMetadataBundle", () => { const fetcher = async () => mockDescribeResponse({ columns: [ - // Time-grain on a measure should not be picked up — measures never - // carry time_grain in the YAML 1.1 spec; defending here is belt- - // and-suspenders, in case DESCRIBE leaks a stray attribute. - { - name: "arr", - type: "DECIMAL", - is_measure: true, - time_grain: ["day"], - }, - { - name: "ts", - type: "TIMESTAMP", - is_measure: false, - time_grain: ["day", "month"], - }, + // Even when a measure resolves to a temporal type (rare but possible + // for MEASURE() expressions like MAX(event_at)), no grains should be + // emitted — measures aren't grouped on. Grain inference is gated on + // is_measure: false in extractMetricColumns. + { name: "last_event_at", type: "TIMESTAMP", is_measure: true }, + { name: "ts", type: "TIMESTAMP", is_measure: false }, ], }); const schemas = await syncMetrics(resolution, fetcher); const bundle = buildMetricsMetadataBundle(schemas); - expect(bundle.revenue.measures.arr.time_grain).toBeUndefined(); - expect(bundle.revenue.dimensions.ts.time_grain).toEqual(["day", "month"]); + expect(bundle.revenue.measures.last_event_at.time_grain).toBeUndefined(); + expect(bundle.revenue.dimensions.ts.time_grain).toEqual([ + "day", + "hour", + "minute", + "month", + "quarter", + "week", + "year", + ]); }); }); @@ -692,7 +722,6 @@ describe("generateMetricsMetadataJson — snapshot", () => { type: "TIMESTAMP", is_measure: false, display_name: "Period", - time_grain: ["day", "week", "month", "quarter"], }, ], }) @@ -725,12 +754,16 @@ describe("generateMetricsMetadataJson — snapshot", () => { expect(parsed.revenue.measures.arr.display_name).toBe( "Annual Recurring Revenue", ); - // Time grains are sorted lexicographically by extractMetricColumns (Phase 2). + // Time grains are inferred from the SQL type and ordered lexicographically. + // TIMESTAMP → all 7 standard grains. expect(parsed.revenue.dimensions.created_at.time_grain).toEqual([ "day", + "hour", + "minute", "month", "quarter", "week", + "year", ]); expect(parsed.customer_metrics.lane).toBe("obo"); }); @@ -742,7 +775,7 @@ describe("generateMetricsMetadataJson — snapshot", () => { // ── Phase 2: syncMetrics propagates timeGrains end-to-end ──────────────── describe("syncMetrics — time-typed dimension propagation", () => { - test("propagates the time_grain attribute onto the resulting MetricSchema", async () => { + test("propagates inferred grains onto the resulting MetricSchema", async () => { const resolution = resolveMetricConfig({ sp: { revenue: { source: "demo.public.revenue" } }, }); @@ -751,12 +784,7 @@ describe("syncMetrics — time-typed dimension propagation", () => { mockDescribeResponse({ columns: [ { name: "arr", type: "DECIMAL", is_measure: true }, - { - name: "ts", - type: "TIMESTAMP", - is_measure: false, - time_grain: ["day", "month"], - }, + { name: "ts", type: "TIMESTAMP", is_measure: false }, { name: "region", type: "STRING", is_measure: false }, ], }); @@ -764,7 +792,15 @@ describe("syncMetrics — time-typed dimension propagation", () => { const schemas = await syncMetrics(resolution, fetcher); expect(schemas[0].dimensions).toHaveLength(2); const tsDim = schemas[0].dimensions.find((d) => d.name === "ts"); - expect(tsDim?.timeGrains).toEqual(["day", "month"]); + expect(tsDim?.timeGrains).toEqual([ + "day", + "hour", + "minute", + "month", + "quarter", + "week", + "year", + ]); const regionDim = schemas[0].dimensions.find((d) => d.name === "region"); expect(regionDim?.timeGrains).toBeUndefined(); }); From eefeadc78fccf3375f3aef0e5dbf708353b867f1 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Thu, 30 Apr 2026 10:22:04 +0200 Subject: [PATCH 13/34] fix(appkit): wire metrics.metadata.json into the server-side metric registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 5 emitted a build-time metadata bundle (`metrics.metadata.json`) but the server's `loadMetricRegistry()` was called without it. Result: `knownTimeGrainsByDim` was always `{}` at runtime, so the validator rejected every legit `timeGrain: "month"` request with "no time-typed dimension is included in 'dimensions'" — the user's regenerated bundle sat on disk unread. Two fixes: 1. **Auto-discover the metadata bundle** — `loadMetricRegistry()` now reads `/shared/appkit-types/metrics.metadata.json` (matching the default `metricMetadataOutFile` from the type-gen Vite plugin) when the caller doesn't pass metadata explicitly. New `readMetricsMetadataBundle()` helper reads, parses, and transforms the bundle's per-column shape (`{ measures: { [k]: { type, ... } }, dimensions: { [k]: { type, time_grain?, ... } } }`) into the simpler `MetricBuildTimeMetadata` shape the registry expects. Also captures `dimensionTypes` from the bundle into `knownDimensionTypes` so the filter validator's op-vs-type compatibility checks light up. 2. **Fall-open behavior on the cross-field timeGrain rule** — when `knownTimeGrainsByDim` is empty (no metadata available), the validator no longer rejects requests carrying `timeGrain`. Mirrors the existing dimensions-fall-open behavior at `metric.ts:274-281`. The warehouse handles incompatible grains itself. Same fall-open applied to `buildMetricSql`'s mirror check. Behavior on disk now matches the v1 PRD intent end-to-end: type-gen emits the bundle → server reads it on boot → validator narrows `timeGrain` to the YAML-allowed set per dimension. When the bundle is absent, the request is accepted and warehouse rejection becomes the late-binding signal. Tests: existing "rejects timeGrain when no time-typed dim" updated to test the metadata-available case + new "falls open when metadata empty" test covering the new contract. 2086/2086 tests pass; full backpressure green. Co-authored-by: Isaac Signed-off-by: Atila Fassina --- .../appkit/src/plugins/analytics/metric.ts | 137 +++++++++++++++++- .../plugins/analytics/tests/metric.test.ts | 30 +++- 2 files changed, 156 insertions(+), 11 deletions(-) diff --git a/packages/appkit/src/plugins/analytics/metric.ts b/packages/appkit/src/plugins/analytics/metric.ts index 897e88ad5..7a3817807 100644 --- a/packages/appkit/src/plugins/analytics/metric.ts +++ b/packages/appkit/src/plugins/analytics/metric.ts @@ -90,6 +90,15 @@ const NULL_OPERATORS = new Set(["set", "notSet"]); */ const QUERIES_DIR = path.resolve(process.cwd(), "config/queries"); const METRIC_CONFIG_FILE = "metric.json"; +/** + * Default location of the build-time metadata bundle emitted by + * `metric sync` and the Vite type-generator plugin. The path mirrors the + * default `metricMetadataOutFile` in `packages/appkit/src/type-generator/`. + */ +const METRIC_METADATA_PATH = path.resolve( + process.cwd(), + "shared/appkit-types/metrics.metadata.json", +); const METRIC_KEY_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/; const FQN_PATTERN = @@ -144,6 +153,103 @@ interface MetricBuildTimeMetadata { * dimensions; regular dimensions are absent from this map. */ timeGrainsByDim?: Record; + /** + * Dimension name → SQL type. Drives op-vs-type compatibility checks in the + * filter validator. Empty/missing → validator falls open on type checks. + */ + dimensionTypes?: Record; +} + +/** + * Read the build-time metadata bundle (`metrics.metadata.json`) emitted by + * `metric sync` / the Vite type-generator plugin, and transform it into the + * shape `loadMetricRegistry` expects. + * + * Returns `null` when the file is absent — apps that haven't run `metric sync` + * fall back to the validator's open mode. Logs and returns null on parse + * failures so a stale bundle never takes the server down. + */ +async function readMetricsMetadataBundle( + metadataPath: string = METRIC_METADATA_PATH, +): Promise | null> { + let raw: string; + try { + raw = await fs.readFile(metadataPath, "utf8"); + } catch (err) { + if ((err as NodeJS.ErrnoException).code === "ENOENT") { + return null; + } + logger.warn( + "Failed to read metrics.metadata.json at %s: %s", + metadataPath, + err instanceof Error ? err.message : String(err), + ); + return null; + } + + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch (err) { + logger.warn( + "metrics.metadata.json at %s is not valid JSON: %s", + metadataPath, + err instanceof Error ? err.message : String(err), + ); + return null; + } + + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + return null; + } + + const result: Record = {}; + for (const [metricKey, value] of Object.entries( + parsed as Record, + )) { + if (!value || typeof value !== "object") continue; + const v = value as Record; + const measuresObj = + v.measures && typeof v.measures === "object" && !Array.isArray(v.measures) + ? (v.measures as Record) + : {}; + const dimensionsObj = + v.dimensions && + typeof v.dimensions === "object" && + !Array.isArray(v.dimensions) + ? (v.dimensions as Record) + : {}; + + const measures = Object.keys(measuresObj).sort(); + const dimensions = Object.keys(dimensionsObj).sort(); + + const timeGrainsByDim: Record = {}; + const dimensionTypes: Record = {}; + for (const [dimName, dimMeta] of Object.entries(dimensionsObj)) { + if (!dimMeta || typeof dimMeta !== "object") continue; + const m = dimMeta as Record; + if (typeof m.type === "string") { + dimensionTypes[dimName] = m.type; + } + if (Array.isArray(m.time_grain)) { + const grains = m.time_grain.filter( + (g): g is string => typeof g === "string", + ); + if (grains.length > 0) { + timeGrainsByDim[dimName] = grains; + } + } + } + + result[metricKey] = { + measures, + dimensions, + timeGrainsByDim, + dimensionTypes, + }; + } + + return result; } /** @@ -163,6 +269,13 @@ export async function loadMetricRegistry( ): Promise> { const metricPath = path.join(queriesDir, METRIC_CONFIG_FILE); + // Auto-discover the build-time metadata bundle if the caller didn't + // pass one explicitly. This wires up Phase 5's metrics.metadata.json + // to the server-side validator so it knows which dimensions are time- + // typed (and therefore which `timeGrain` values to accept). + const resolvedMetadata = + metadata ?? (await readMetricsMetadataBundle()) ?? undefined; + let raw: string; try { raw = await fs.readFile(metricPath, "utf8"); @@ -203,7 +316,7 @@ export async function loadMetricRegistry( `Duplicate metric key "${key}": cannot appear in both sp and obo lanes.`, ); } - const meta = metadata?.[key]; + const meta = resolvedMetadata?.[key]; registry[key] = { key, source: entry.source, @@ -211,6 +324,7 @@ export async function loadMetricRegistry( knownMeasures: meta?.measures ?? [], knownDimensions: meta?.dimensions ?? [], knownTimeGrainsByDim: meta?.timeGrainsByDim ?? {}, + knownDimensionTypes: meta?.dimensionTypes, }; } } @@ -357,7 +471,11 @@ export function makeMetricRequestSchema( // are available; values cardinality is enforced per operator; AND/OR // nesting is capped at METRIC_FILTER_MAX_DEPTH. return baseObject.superRefine((value, ctx) => { - if (value.timeGrain != null) { + // Cross-field rule for timeGrain. We can only enforce "no time-typed + // dimension is grouped" when metadata is available — without it the + // validator falls open (mirrors the dimensions-fall-open behavior at + // line 274-281). The warehouse will reject incompatible grains itself. + if (value.timeGrain != null && Object.keys(grainsByDim).length > 0) { const dims = value.dimensions ?? []; const hasTimeDim = dims.some( (d) => Array.isArray(grainsByDim[d]) && grainsByDim[d].length > 0, @@ -751,11 +869,18 @@ export function buildMetricSql( `Refusing to build SQL: unknown timeGrain "${request.timeGrain}" for metric "${registration.key}".`, ); } - const hasTimeDim = dimensions.some((d) => isTimeTypedDim(registration, d)); - if (!hasTimeDim) { - throw new Error( - `Refusing to build SQL: timeGrain "${request.timeGrain}" set but no time-typed dimension is in 'dimensions'.`, + // Same fall-open rule as the validator: only enforce when metadata is + // available. Without registry knowledge we trust the warehouse to reject + // an incompatible grain at SQL execution time. + if (Object.keys(registration.knownTimeGrainsByDim).length > 0) { + const hasTimeDim = dimensions.some((d) => + isTimeTypedDim(registration, d), ); + if (!hasTimeDim) { + throw new Error( + `Refusing to build SQL: timeGrain "${request.timeGrain}" set but no time-typed dimension is in 'dimensions'.`, + ); + } } } diff --git a/packages/appkit/src/plugins/analytics/tests/metric.test.ts b/packages/appkit/src/plugins/analytics/tests/metric.test.ts index 9e56fff34..a0c7fc68a 100644 --- a/packages/appkit/src/plugins/analytics/tests/metric.test.ts +++ b/packages/appkit/src/plugins/analytics/tests/metric.test.ts @@ -245,20 +245,40 @@ describe("metric — pure helpers", () => { ).toThrowError(/no time-typed dimension/); }); - test("rejects timeGrain when the metric view has no time-typed dims", () => { - const noTimeRegistration: MetricRegistration = { + test("rejects timeGrain when none of the requested dims are time-typed (metadata available)", () => { + // Some dims are time-typed in the registry, but the request only + // includes a non-time dim. The validator must catch the mismatch. + const partialRegistration: MetricRegistration = { ...REVENUE_REGISTRATION, - knownDimensions: ["region", "segment"], - knownTimeGrainsByDim: {}, + knownDimensions: ["region", "segment", "created_at"], + knownTimeGrainsByDim: { created_at: ["day", "week", "month"] }, }; expect(() => - validateMetricRequest(noTimeRegistration, { + validateMetricRequest(partialRegistration, { measures: ["arr"], dimensions: ["region"], timeGrain: "month", }), ).toThrowError(); }); + + test("falls open on timeGrain when metadata is empty (no metrics.metadata.json)", () => { + // Without build-time metadata the validator can't tell which dims are + // time-typed. Mirror the dimensions-fall-open behavior: accept the + // request and let the warehouse reject incompatible grains. + const noMetadataRegistration: MetricRegistration = { + ...REVENUE_REGISTRATION, + knownDimensions: [], + knownTimeGrainsByDim: {}, + }; + expect(() => + validateMetricRequest(noMetadataRegistration, { + measures: ["arr"], + dimensions: ["created_at"], + timeGrain: "month", + }), + ).not.toThrowError(); + }); }); describe("buildMetricSql", () => { From 5ed56dac1982cca99baeb8433bf8dcb638c85fb1 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Thu, 30 Apr 2026 10:32:03 +0200 Subject: [PATCH 14/34] fix(playground): defensive optional-chaining on metadata.
    {formatValue( row.churn_rate, - metadata?.measures.churn_rate.format, + metadata?.measures.churn_rate?.format, )}