From 5e4835bc96ecb95eb9654836a61851acf61db73f Mon Sep 17 00:00:00 2001 From: Makisuo Date: Thu, 13 Aug 2026 19:01:08 +0200 Subject: [PATCH 01/13] refactor(query-model): extract the shared query primitives into a leaf package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dashboard widgets and alert rules both persist a query-builder draft, but the schema lived in `@maple/domain/http` — which `@maple/widgets` cannot import, since domain's `MapleApi` embeds the widget schemas and the dependency runs the other way. `packages/widgets/src/index.ts` named this exact blocker: > it stays in `@maple/domain/http` until it gets a leaf of its own. This is that leaf. `@maple/query-model` depends on `@maple/primitives` and `effect` only, so both halves can reach it. Moved in: the query-draft schemas, `TimeRangeSchema` (a neutral value type that alert previews, MCP and the explore pages all resolve), and the metric-type / data-source / signal-source literal unions that were declared twice. New: `QueryBuilderFormulaSchema` replacing three spellings of the same struct, `QueryComparisonSchema`, `QueryResultShape`, `QuerySetSchema` (queries + formulas + comparison — the value both surfaces will store), and one reducer table deriving both the widget spelling and the alert spelling. Two things deliberately NOT collapsed: - The editor-state draft interface in `@maple/query-engine/query-builder` keeps every field required while the stored schema keeps them optional. That split is the point: the builder always holds a populated draft, a stored one omits what the user never set. Merging them would make `whereClause` possibly- undefined at every read in the builder to buy nothing. - `SERIES_REDUCERS` and `ALERT_REDUCERS` stay distinct literal sets. Widgets persist "first", alert rules persist "identity", and they coincide only on a one-bucket window — merging them would rewrite stored values on both sides. Only the table they derive from is shared, which keeps the mapping total. Pure move: every previous export is re-exported from its old home, so no call site changes. 41/41 typecheck tasks pass. --- bun.lock | 18 ++ packages/domain/package.json | 1 + packages/domain/src/http/query-engine.ts | 70 ++------ packages/query-engine/package.json | 1 + .../query-engine/src/query-builder/model.ts | 48 ++++-- packages/query-model/package.json | 22 +++ packages/query-model/src/comparison.ts | 17 ++ packages/query-model/src/formula.ts | 18 ++ packages/query-model/src/index.ts | 26 +++ packages/query-model/src/query-draft.ts | 81 +++++++++ packages/query-model/src/query-model.test.ts | 157 ++++++++++++++++++ packages/query-model/src/query-set.ts | 27 +++ packages/query-model/src/result-shape.ts | 14 ++ packages/query-model/src/series-reducer.ts | 45 +++++ packages/query-model/src/time-range.ts | 24 +++ packages/query-model/tsconfig.json | 23 +++ packages/widgets/package.json | 1 + .../src/dashboard/shared/time-range.ts | 20 +-- 18 files changed, 526 insertions(+), 87 deletions(-) create mode 100644 packages/query-model/package.json create mode 100644 packages/query-model/src/comparison.ts create mode 100644 packages/query-model/src/formula.ts create mode 100644 packages/query-model/src/index.ts create mode 100644 packages/query-model/src/query-draft.ts create mode 100644 packages/query-model/src/query-model.test.ts create mode 100644 packages/query-model/src/query-set.ts create mode 100644 packages/query-model/src/result-shape.ts create mode 100644 packages/query-model/src/series-reducer.ts create mode 100644 packages/query-model/src/time-range.ts create mode 100644 packages/query-model/tsconfig.json diff --git a/bun.lock b/bun.lock index a9e122908..195fdecb5 100644 --- a/bun.lock +++ b/bun.lock @@ -591,6 +591,7 @@ "dependencies": { "@maple-dev/clickhouse-builder": "workspace:*", "@maple/primitives": "workspace:*", + "@maple/query-model": "workspace:*", "@maple/widgets": "workspace:*", "@tinybirdco/sdk": "catalog:tinybird", "effect": "catalog:effect", @@ -660,6 +661,7 @@ "@maple-dev/clickhouse-builder": "workspace:*", "@maple/cache": "workspace:*", "@maple/domain": "workspace:*", + "@maple/query-model": "workspace:*", "effect": "catalog:effect", }, "devDependencies": { @@ -684,6 +686,19 @@ "vitest": "catalog:", }, }, + "packages/query-model": { + "name": "@maple/query-model", + "dependencies": { + "@maple/primitives": "workspace:*", + "effect": "catalog:effect", + }, + "devDependencies": { + "@effect/language-service": "catalog:effect", + "@types/node": "catalog:tooling", + "typescript": "catalog:tooling", + "vitest": "catalog:", + }, + }, "packages/ui": { "name": "@maple/ui", "dependencies": { @@ -723,6 +738,7 @@ "name": "@maple/widgets", "dependencies": { "@maple/primitives": "workspace:*", + "@maple/query-model": "workspace:*", "effect": "catalog:effect", }, "devDependencies": { @@ -1592,6 +1608,8 @@ "@maple/query-engine-integrations": ["@maple/query-engine-integrations@workspace:packages/query-engine-integrations"], + "@maple/query-model": ["@maple/query-model@workspace:packages/query-model"], + "@maple/scraper": ["@maple/scraper@workspace:apps/scraper"], "@maple/thinking-orbs": ["@maple/thinking-orbs@workspace:lib/thinking-orbs"], diff --git a/packages/domain/package.json b/packages/domain/package.json index e3e422622..faac62244 100644 --- a/packages/domain/package.json +++ b/packages/domain/package.json @@ -37,6 +37,7 @@ "dependencies": { "@maple-dev/clickhouse-builder": "workspace:*", "@maple/primitives": "workspace:*", + "@maple/query-model": "workspace:*", "@maple/widgets": "workspace:*", "@tinybirdco/sdk": "catalog:tinybird", "effect": "catalog:effect" diff --git a/packages/domain/src/http/query-engine.ts b/packages/domain/src/http/query-engine.ts index f298683b8..123e58c4d 100644 --- a/packages/domain/src/http/query-engine.ts +++ b/packages/domain/src/http/query-engine.ts @@ -1560,66 +1560,20 @@ export class WorkloadInfraTimeseriesResponse extends Schema.Class + QueryBuilderAddOnsSchema, + type QueryBuilderQueryDraftPayload, + QueryBuilderQueryDraftSchema, + TracesQueryDraftSchema, +} from "@maple/query-model" // Raw SQL chart (Hyperdx-style — user-authored ClickHouse SQL with macros) diff --git a/packages/query-engine/package.json b/packages/query-engine/package.json index f7bfb3698..377dcfe7f 100644 --- a/packages/query-engine/package.json +++ b/packages/query-engine/package.json @@ -30,6 +30,7 @@ "@maple-dev/clickhouse-builder": "workspace:*", "@maple/cache": "workspace:*", "@maple/domain": "workspace:*", + "@maple/query-model": "workspace:*", "effect": "catalog:effect" }, "devDependencies": { diff --git a/packages/query-engine/src/query-builder/model.ts b/packages/query-engine/src/query-builder/model.ts index 80caf9709..0549a7c7e 100644 --- a/packages/query-engine/src/query-builder/model.ts +++ b/packages/query-engine/src/query-builder/model.ts @@ -3,11 +3,35 @@ import type { QuerySpec } from "@maple/domain/query-engine" import { normalizeKey, parseBoolean, parseWhereClause, splitCsv } from "@maple/domain/where-clause" import { Match } from "effect" -export type QueryBuilderDataSource = "traces" | "logs" | "metrics" +export type { + QueryBuilderDataSource, + QueryBuilderMetricType, + QueryBuilderSignalSource, +} from "@maple/query-model" +import { QUERY_BUILDER_METRIC_TYPES } from "@maple/query-model" +import type { + QueryBuilderDataSource, + QueryBuilderFormulaPayload, + QueryBuilderMetricType, + QueryBuilderSignalSource, +} from "@maple/query-model" + export type QueryBuilderAddOnKey = "groupBy" | "having" | "orderBy" | "limit" | "legend" -export type QueryBuilderMetricType = "sum" | "gauge" | "histogram" | "exponential_histogram" -export type QueryBuilderSignalSource = "default" | "meter" +/** + * EDITOR state, not the stored payload. + * + * Every field is required here and `Schema.optional` in + * `QueryBuilderQueryDraftSchema` (`@maple/query-model`), and that difference is + * the point: the builder always holds a fully-populated draft — an empty + * where-clause is `""`, not absent — while a stored or wire draft omits what the + * user never set. Collapsing this into the schema's inferred type would make + * `query.whereClause` possibly-undefined at every read in the builder to buy + * nothing. + * + * `normalizeRuleQueryDraft` / `toInitialState` are the boundary that fills a + * payload out into this shape. + */ interface QueryBuilderQueryDraftBase { id: string name: string @@ -123,12 +147,7 @@ export function resetAggregationForMetricType( return validOptions[0]?.value ?? "avg" } -export const QUERY_BUILDER_METRIC_TYPES: readonly QueryBuilderMetricType[] = [ - "sum", - "gauge", - "histogram", - "exponential_histogram", -] as const +export { QUERY_BUILDER_METRIC_TYPES } export const GROUP_BY_OPTIONS: Record> = { traces: [ @@ -197,11 +216,12 @@ export function createQueryDraft(index: number): TracesQueryDraft { } } -export interface QueryBuilderFormulaDraft { - id: string - name: string - expression: string - legend: string +/** + * Editor state for a formula, same total-vs-partial split as + * `QueryBuilderQueryDraft`: `hidden` is always set here and optional in the + * stored `QueryBuilderFormulaSchema`. + */ +export interface QueryBuilderFormulaDraft extends QueryBuilderFormulaPayload { hidden: boolean } diff --git a/packages/query-model/package.json b/packages/query-model/package.json new file mode 100644 index 000000000..eb2d6c11d --- /dev/null +++ b/packages/query-model/package.json @@ -0,0 +1,22 @@ +{ + "name": "@maple/query-model", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@maple/primitives": "workspace:*", + "effect": "catalog:effect" + }, + "devDependencies": { + "@effect/language-service": "catalog:effect", + "@types/node": "catalog:tooling", + "typescript": "catalog:tooling", + "vitest": "catalog:" + } +} diff --git a/packages/query-model/src/comparison.ts b/packages/query-model/src/comparison.ts new file mode 100644 index 000000000..52ad8e290 --- /dev/null +++ b/packages/query-model/src/comparison.ts @@ -0,0 +1,17 @@ +import { Schema } from "effect" + +/** + * Whether a query set also fetches a shifted window to compare against. + * + * Dashboards support this; alert rules do not — a rule evaluates the current + * window only, and `normalizeRule` rejects `previous_period` rather than + * silently dropping it. + */ +export const QUERY_COMPARISON_MODES = ["none", "previous_period"] as const +export type QueryComparisonMode = (typeof QUERY_COMPARISON_MODES)[number] + +export const QueryComparisonSchema = Schema.Struct({ + mode: Schema.optional(Schema.Literals(QUERY_COMPARISON_MODES)), + includePercentChange: Schema.optional(Schema.Boolean), +}) +export type QueryComparisonPayload = Schema.Schema.Type diff --git a/packages/query-model/src/formula.ts b/packages/query-model/src/formula.ts new file mode 100644 index 000000000..b3f41fec7 --- /dev/null +++ b/packages/query-model/src/formula.ts @@ -0,0 +1,18 @@ +import { Schema } from "effect" + +/** + * A formula over the named queries in the same query set (`A / B`). + * + * One declaration replacing three: a local `FormulaSchema` in the web + * timeseries server function, a TS-only `QueryBuilderFormulaDraft` interface in + * `@maple/query-engine/query-builder`, and a bare `Schema.Array(Schema.Unknown)` + * in the MCP widget inspector. + */ +export const QueryBuilderFormulaSchema = Schema.Struct({ + id: Schema.String, + name: Schema.String, + expression: Schema.String, + legend: Schema.String, + hidden: Schema.optional(Schema.Boolean), +}) +export type QueryBuilderFormulaPayload = Schema.Schema.Type diff --git a/packages/query-model/src/index.ts b/packages/query-model/src/index.ts new file mode 100644 index 000000000..753ddfccf --- /dev/null +++ b/packages/query-model/src/index.ts @@ -0,0 +1,26 @@ +// @maple/query-model — what "a warehouse query" is, for every surface that stores one. +// +// The leaf `packages/widgets/src/index.ts` asked for: dashboard widgets and +// alert rules both persist a query-builder draft, so the draft belongs below +// both rather than inside either. Deps are `@maple/primitives` and `effect` +// only, which is what lets `@maple/widgets` (itself below `@maple/domain`) +// import it. +// +// It goes in `packages/`, not `lib/`: it names `traces`/`logs`/`metrics` and +// Maple's aggregation vocabulary, so it fails the "could ship as a standalone +// OSS library tomorrow" test. +// +// Deliberately NOT here: +// - Lowering. Turning a draft into a `QuerySpec` needs the CH DSL and lives in +// `@maple/query-engine/query-builder`. +// - Aggregation option lists (`AGGREGATIONS_BY_SOURCE`, `GROUP_BY_OPTIONS`). +// Those are builder-UI affordances keyed off what the lowering implements, +// not part of the stored value. + +export * from "./comparison" +export * from "./formula" +export * from "./query-draft" +export * from "./query-set" +export * from "./result-shape" +export * from "./series-reducer" +export * from "./time-range" diff --git a/packages/query-model/src/query-draft.ts b/packages/query-model/src/query-draft.ts new file mode 100644 index 000000000..87cc2c2ca --- /dev/null +++ b/packages/query-model/src/query-draft.ts @@ -0,0 +1,81 @@ +import { Schema } from "effect" + +/** + * A query-builder draft: the editable description of one warehouse query. + * + * Persisted by BOTH dashboard widgets (`WidgetDataSourceV3`, `kind: "query"`) + * and alert rules (`alert_rules.query_builder_draft_json`). That shared + * ownership is why this lives in a leaf package rather than in `@maple/domain` + * or `@maple/widgets` — `@maple/widgets` sits below `@maple/domain` (domain's + * `MapleApi` embeds the widget schemas), so a widget schema cannot reach up into + * domain for it. + */ + +export const QUERY_BUILDER_DATA_SOURCES = ["traces", "logs", "metrics"] as const +export type QueryBuilderDataSource = (typeof QUERY_BUILDER_DATA_SOURCES)[number] + +export const QUERY_BUILDER_METRIC_TYPES = ["sum", "gauge", "histogram", "exponential_histogram"] as const +export type QueryBuilderMetricType = (typeof QUERY_BUILDER_METRIC_TYPES)[number] + +export const QUERY_BUILDER_SIGNAL_SOURCES = ["default", "meter"] as const +export type QueryBuilderSignalSource = (typeof QUERY_BUILDER_SIGNAL_SOURCES)[number] + +export const QueryBuilderAddOnsSchema = Schema.Struct({ + groupBy: Schema.Boolean, + having: Schema.Boolean, + orderBy: Schema.Boolean, + limit: Schema.Boolean, + legend: Schema.Boolean, +}) + +// Fields shared by every query-draft source. Metric-specific fields live only +// on the metrics variant below — traces/logs queries never carry them. +const queryDraftBaseFields = { + id: Schema.String, + name: Schema.String, + enabled: Schema.optional(Schema.Boolean), + hidden: Schema.optional(Schema.Boolean), + whereClause: Schema.optional(Schema.String), + aggregation: Schema.String, + stepInterval: Schema.optional(Schema.String), + orderByDirection: Schema.optional(Schema.Literals(["desc", "asc"])), + addOns: Schema.optional(QueryBuilderAddOnsSchema), + groupBy: Schema.optional(Schema.mutable(Schema.Array(Schema.String))), + having: Schema.optional(Schema.String), + orderBy: Schema.optional(Schema.String), + limit: Schema.optional(Schema.String), + // Opt-in top-N series cap for group-by timeseries charts (entered as a string + // in the builder; parsed to a positive integer when lowering to a QuerySpec). + seriesLimit: Schema.optional(Schema.String), + legend: Schema.optional(Schema.String), +} + +export const TracesQueryDraftSchema = Schema.Struct({ + ...queryDraftBaseFields, + dataSource: Schema.Literal("traces"), + // A non-empty `valueField` (e.g. "attr.result.rowCount") switches the traces + // query into numeric-attribute aggregation mode: `aggregation` becomes a + // numeric function over that span attribute instead of a duration-based metric. + valueField: Schema.optional(Schema.String), +}) + +export const LogsQueryDraftSchema = Schema.Struct({ + ...queryDraftBaseFields, + dataSource: Schema.Literal("logs"), +}) + +export const MetricsQueryDraftSchema = Schema.Struct({ + ...queryDraftBaseFields, + dataSource: Schema.Literal("metrics"), + signalSource: Schema.optional(Schema.Literals(QUERY_BUILDER_SIGNAL_SOURCES)), + metricName: Schema.optional(Schema.String), + metricType: Schema.optional(Schema.Literals(QUERY_BUILDER_METRIC_TYPES)), + isMonotonic: Schema.optional(Schema.Boolean), +}) + +export const QueryBuilderQueryDraftSchema = Schema.Union([ + TracesQueryDraftSchema, + LogsQueryDraftSchema, + MetricsQueryDraftSchema, +]) +export type QueryBuilderQueryDraftPayload = Schema.Schema.Type diff --git a/packages/query-model/src/query-model.test.ts b/packages/query-model/src/query-model.test.ts new file mode 100644 index 000000000..a52883f0b --- /dev/null +++ b/packages/query-model/src/query-model.test.ts @@ -0,0 +1,157 @@ +import { Schema } from "effect" +import { describe, expect, it } from "vitest" +import { + ALERT_REDUCER_TO_SERIES_REDUCER, + ALERT_REDUCERS, + QuerySetSchema, + QueryBuilderQueryDraftSchema, + SERIES_REDUCERS, + TimeRangeSchema, +} from "./index" + +const decodeDraft = Schema.decodeUnknownSync(QueryBuilderQueryDraftSchema) +const decodeQuerySet = Schema.decodeUnknownSync(QuerySetSchema) +const decodeTimeRange = Schema.decodeUnknownSync(TimeRangeSchema) + +describe("QueryBuilderQueryDraftSchema", () => { + it("decodes a minimal draft for every data source", () => { + for (const dataSource of ["traces", "logs", "metrics"] as const) { + const decoded = decodeDraft({ id: "a", name: "A", aggregation: "count", dataSource }) + expect(decoded.dataSource).toBe(dataSource) + } + }) + + it("keeps every optional field absent rather than defaulting it", () => { + // The stored shape is partial on purpose — the editor-state type in + // `@maple/query-engine/query-builder` is the total one. A default here + // would write fields the user never set back into their document. + const decoded = decodeDraft({ id: "a", name: "A", aggregation: "count", dataSource: "traces" }) + expect(decoded).toEqual({ id: "a", name: "A", aggregation: "count", dataSource: "traces" }) + }) + + it("round-trips a fully-populated metrics draft", () => { + const draft = { + id: "q1", + name: "A", + enabled: true, + hidden: false, + whereClause: "service.name = 'api'", + aggregation: "rate", + stepInterval: "1m", + orderByDirection: "desc" as const, + addOns: { groupBy: true, having: false, orderBy: false, limit: false, legend: false }, + groupBy: ["service.name"], + having: "", + orderBy: "", + limit: "10", + seriesLimit: "5", + legend: "{{service.name}}", + dataSource: "metrics" as const, + signalSource: "default" as const, + metricName: "http.server.duration", + metricType: "histogram" as const, + isMonotonic: false, + } + expect(decodeDraft(draft)).toEqual(draft) + }) + + it("rejects an unknown data source", () => { + expect(() => decodeDraft({ id: "a", name: "A", aggregation: "count", dataSource: "spans" })).toThrow() + }) + + it("carries the traces-only valueField", () => { + const traces = decodeDraft({ + id: "a", + name: "A", + aggregation: "avg", + dataSource: "traces", + valueField: "attr.result.rowCount", + }) + expect(traces).toMatchObject({ valueField: "attr.result.rowCount" }) + }) + + it("drops an unknown key instead of rejecting the draft", () => { + // Leniency is deliberate for a STORED schema: a draft written by a newer + // client, or one carrying a field since removed, must still decode — a + // rejection here propagates up to `parseStoredDashboard` and locks the + // whole dashboard out of editing. Unknown keys are dropped, not kept, so + // the next read-modify-write doesn't persist them back. + const decoded = decodeDraft({ + id: "a", + name: "A", + aggregation: "avg", + dataSource: "logs", + valueField: "x", + somethingFromTheFuture: 1, + }) + expect(decoded).not.toHaveProperty("valueField") + expect(decoded).not.toHaveProperty("somethingFromTheFuture") + }) +}) + +describe("QuerySetSchema", () => { + it("decodes a bare list of queries", () => { + const set = decodeQuerySet({ + queries: [{ id: "a", name: "A", aggregation: "count", dataSource: "traces" }], + }) + expect(set.queries).toHaveLength(1) + expect(set.formulas).toBeUndefined() + expect(set.comparison).toBeUndefined() + }) + + it("decodes formulas and a comparison window", () => { + const set = decodeQuerySet({ + queries: [ + { id: "a", name: "A", aggregation: "count", dataSource: "traces" }, + { id: "b", name: "B", aggregation: "count", dataSource: "traces" }, + ], + formulas: [{ id: "f1", name: "F1", expression: "A / B", legend: "ratio" }], + comparison: { mode: "previous_period", includePercentChange: true }, + }) + expect(set.formulas?.[0]?.expression).toBe("A / B") + expect(set.comparison?.mode).toBe("previous_period") + }) + + it("accepts an empty query list", () => { + // A widget mid-edit can legitimately have no queries, and a stored schema + // that rejects is a stored schema that locks the document out of editing. + expect(decodeQuerySet({ queries: [] }).queries).toEqual([]) + }) +}) + +describe("series reducers", () => { + it("maps every alert reducer onto a series reducer", () => { + for (const reducer of ALERT_REDUCERS) { + expect(SERIES_REDUCERS).toContain(ALERT_REDUCER_TO_SERIES_REDUCER[reducer]) + } + }) + + it("keeps the two spellings distinct where they genuinely differ", () => { + // Merging these sets would rewrite stored values: widgets persist "first", + // alert rules persist "identity", and they coincide only on one bucket. + expect(ALERT_REDUCER_TO_SERIES_REDUCER.identity).toBe("first") + expect(SERIES_REDUCERS).toContain("count") + expect(ALERT_REDUCERS).not.toContain("count" as never) + }) +}) + +describe("TimeRangeSchema", () => { + it("decodes both variants", () => { + expect(decodeTimeRange({ type: "relative", value: "24h" })).toEqual({ + type: "relative", + value: "24h", + }) + const absolute = decodeTimeRange({ + type: "absolute", + startTime: "2026-08-01T00:00:00.000Z", + endTime: "2026-08-02T00:00:00.000Z", + }) + expect(absolute.type).toBe("absolute") + }) + + it("rejects an absolute range whose instants are not ISO date-times", () => { + expect(() => + decodeTimeRange({ type: "absolute", startTime: "yesterday", endTime: "today" }), + ).toThrow() + }) +}) diff --git a/packages/query-model/src/query-set.ts b/packages/query-model/src/query-set.ts new file mode 100644 index 000000000..6a4f406e0 --- /dev/null +++ b/packages/query-model/src/query-set.ts @@ -0,0 +1,27 @@ +import { Schema } from "effect" +import { QueryComparisonSchema } from "./comparison" +import { QueryBuilderFormulaSchema } from "./formula" +import { QueryBuilderQueryDraftSchema } from "./query-draft" + +/** + * "A warehouse query", as both dashboard widgets and alert rules store it. + * + * The model stays rich — N queries plus formulas plus an optional comparison + * window — and the surfaces that can't express all of it constrain at + * validation time rather than by lossy conversion. An alert rule evaluates one + * series, so `normalizeRule` fails a set with two enabled queries or any + * formula with a named error; it never silently keeps the first one. That is + * what makes "create an alert from this chart" field copying rather than + * translation. + * + * No `check`s here. A stored schema that can reject is a stored schema that can + * lock a document out of editing, and the widget document is read through + * `parseStoredDashboard` on the writable path. Size and shape limits belong at + * the execute boundary. + */ +export const QuerySetSchema = Schema.Struct({ + queries: Schema.Array(QueryBuilderQueryDraftSchema), + formulas: Schema.optional(Schema.Array(QueryBuilderFormulaSchema)), + comparison: Schema.optional(QueryComparisonSchema), +}) +export type QuerySet = Schema.Schema.Type diff --git a/packages/query-model/src/result-shape.ts b/packages/query-model/src/result-shape.ts new file mode 100644 index 000000000..9f04a4945 --- /dev/null +++ b/packages/query-model/src/result-shape.ts @@ -0,0 +1,14 @@ +import { Schema } from "effect" + +/** + * What a query set is asked to return. + * + * Carried alongside the queries rather than encoded in a per-shape union: a + * widget switching from a timeseries to a breakdown keeps the same drafts, and + * a union keyed on the shape would turn that switch into a decode failure for + * any key the new arm doesn't declare. + */ +export const QUERY_RESULT_SHAPES = ["timeseries", "breakdown", "list"] as const +export type QueryResultShape = (typeof QUERY_RESULT_SHAPES)[number] + +export const QueryResultShapeSchema = Schema.Literals(QUERY_RESULT_SHAPES) diff --git a/packages/query-model/src/series-reducer.ts b/packages/query-model/src/series-reducer.ts new file mode 100644 index 000000000..51f41626e --- /dev/null +++ b/packages/query-model/src/series-reducer.ts @@ -0,0 +1,45 @@ +/** + * How a series of buckets collapses to the single number a consumer needs. + * + * Two vocabularies exist and they are NOT merged: widgets persist `"first"` + * (`reduceToValue.aggregate` on a stat or gauge tile) and alert rules persist + * `"identity"` (`alert_rules.reducer`). They coincide only on a one-bucket + * window, so collapsing the literal sets would silently rewrite stored values on + * both sides. + * + * What IS shared is the table below. Both sets are derived from it and the + * mapping between them is total, so adding a reducer is one edit and a reducer + * that has no counterpart fails to compile rather than falling through at + * runtime. + */ +const REDUCER_TABLE = [ + { series: "first", alert: "identity" }, + { series: "sum", alert: "sum" }, + { series: "avg", alert: "avg" }, + { series: "max", alert: "max" }, + { series: "min", alert: "min" }, + // Widget-only: there is no alert reducer that counts buckets, because a rule + // compares a value against a threshold rather than a cardinality. + { series: "count", alert: null }, +] as const satisfies ReadonlyArray<{ series: string; alert: string | null }> + +export type SeriesReducer = (typeof REDUCER_TABLE)[number]["series"] +export type AlertReducer = Exclude<(typeof REDUCER_TABLE)[number]["alert"], null> + +/** Widget spelling. Ordered so `"first"` leads — it is the runtime default. */ +export const SERIES_REDUCERS = REDUCER_TABLE.map((entry) => entry.series) as ReadonlyArray + +/** Alert-rule spelling. */ +export const ALERT_REDUCERS = REDUCER_TABLE.flatMap((entry) => + entry.alert === null ? [] : [entry.alert], +) as ReadonlyArray + +/** Total by construction: every alert reducer has a series counterpart. */ +export const ALERT_REDUCER_TO_SERIES_REDUCER = Object.fromEntries( + REDUCER_TABLE.flatMap((entry) => (entry.alert === null ? [] : [[entry.alert, entry.series] as const])), +) as Record + +/** Partial in the other direction: `"count"` has no alert spelling. */ +export const SERIES_REDUCER_TO_ALERT_REDUCER = Object.fromEntries( + REDUCER_TABLE.flatMap((entry) => (entry.alert === null ? [] : [[entry.series, entry.alert] as const])), +) as Partial> diff --git a/packages/query-model/src/time-range.ts b/packages/query-model/src/time-range.ts new file mode 100644 index 000000000..542a84913 --- /dev/null +++ b/packages/query-model/src/time-range.ts @@ -0,0 +1,24 @@ +import { Schema } from "effect" +import { IsoDateTimeString } from "@maple/primitives" + +/** + * A time window, either anchored to now or pinned to two instants. + * + * Lived in `@maple/widgets` when dashboards were its only writer, but it is a + * neutral value type: alert previews, MCP tools and the explore pages all + * resolve the same shape through `resolveRelativeRange` in + * `@maple/query-engine`. It sits here so none of them has to depend on the + * dashboard document schema to name a time range. + */ +export const TimeRangeSchema = Schema.Union([ + Schema.Struct({ + type: Schema.Literal("relative"), + value: Schema.String, + }), + Schema.Struct({ + type: Schema.Literal("absolute"), + startTime: IsoDateTimeString, + endTime: IsoDateTimeString, + }), +]) +export type TimeRange = typeof TimeRangeSchema.Type diff --git a/packages/query-model/tsconfig.json b/packages/query-model/tsconfig.json new file mode 100644 index 000000000..37cc11429 --- /dev/null +++ b/packages/query-model/tsconfig.json @@ -0,0 +1,23 @@ +{ + "include": ["**/*.ts", "**/*.tsx"], + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["node"], + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "skipLibCheck": true, + "strict": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "plugins": [ + { + "name": "@effect/language-service", + "reportSuggestionsAsWarningsInTsc": true + } + ] + } +} diff --git a/packages/widgets/package.json b/packages/widgets/package.json index bf2092309..71b70bc45 100644 --- a/packages/widgets/package.json +++ b/packages/widgets/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@maple/primitives": "workspace:*", + "@maple/query-model": "workspace:*", "effect": "catalog:effect" }, "devDependencies": { diff --git a/packages/widgets/src/dashboard/shared/time-range.ts b/packages/widgets/src/dashboard/shared/time-range.ts index 7873d1111..915d5eaf9 100644 --- a/packages/widgets/src/dashboard/shared/time-range.ts +++ b/packages/widgets/src/dashboard/shared/time-range.ts @@ -1,15 +1,5 @@ -import { Schema } from "effect" -import { IsoDateTimeString } from "@maple/primitives" - -export const TimeRangeSchema = Schema.Union([ - Schema.Struct({ - type: Schema.Literal("relative"), - value: Schema.String, - }), - Schema.Struct({ - type: Schema.Literal("absolute"), - startTime: IsoDateTimeString, - endTime: IsoDateTimeString, - }), -]) -export type TimeRange = typeof TimeRangeSchema.Type +// `TimeRangeSchema` moved to `@maple/query-model` — alert previews, MCP tools +// and the explore pages all resolve the same shape, so it is not a dashboard +// concept. Re-exported here so `shared/document.ts`, `shared/widget.ts` and +// `@maple/domain/http` keep their existing surface. +export { type TimeRange, TimeRangeSchema } from "@maple/query-model" From 7a3c681d711450e744974468f1dfe3d291263d6a Mon Sep 17 00:00:00 2001 From: Makisuo Date: Thu, 13 Aug 2026 19:07:34 +0200 Subject: [PATCH 02/13] refactor(alerts,query-engine): collapse duplicated bucket sizing, group keys and reducers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five separate spellings of three concepts, found while mapping what dashboards and alerts already share. Bucket sizing had four implementations, not the one the module header claims: - `packages/query-engine/src/datetime.ts` — canonical. - `apps/api/src/routes/v1/query-engine.http.ts` — a private ladder for raw-SQL `$__interval_s`, whose comment pointed at a web path deleted long ago. - `apps/web/.../query-builder-timeseries.ts` — an alias that just called the shared one under a second name. - `apps/mobile/lib/time-utils.ts` — a genuine copy. The api ladder differed from canonical in exactly two ways: a 30-point target and a 300s floor. `ComputeBucketSecondsOptions` now expresses the floor as `minBucketSeconds`, applied by filtering the ladder BEFORE picking rather than clamping after — clamping would round 120 up to 300 while still having chosen "nearest rung" against rungs the caller cannot use. The floor is load-bearing: a sub-5-minute `$__interval_s` produces exactly the scan the granularity was picked to avoid. Tests pin the deleted ladder's outputs. Mobile keeps its copy: `apps/mobile` has no `@maple/*` dependencies at all, and taking one to share a pure function would pull the query engine's module graph into a Metro bundle. Now says so. `alertWindowBucketSeconds` names the window-is-the-bucket rule that was spelled out at both `compileRulePlan` (which bakes it into the stored spec) and `prepareAlertEvaluation`. Those disagreeing would evaluate a different window than the rule was saved with. `toStorageGroupKey` names the engine-vs-storage group-key boundary. The engine emits `"all"` for an ungrouped result; storage, wire and UI spell it `"__total__"`, and an `alert_rule_states` row keyed `"all"` is invisible to every reader. The translation was open-coded at each site that needed it. Reducers were declared five times: the alert literal set, the widget literal set, a shadowing copy in `settings-fields.tsx` hand-maintained separately from the schema that validates it, and a fifth union in `widget-builder-shared.ts`. All now derive from one table. The two literal SETS stay distinct on purpose — widgets persist "first", rules persist "identity" — but a reducer added to one must now declare whether it has a counterpart in the other. 41/41 typecheck, 243 alert tests, 57 datetime tests pass. --- apps/api/src/routes/v1/query-engine.http.ts | 24 +++++------ .../api/src/services/alerts/AlertRuleModel.ts | 22 +++++++++- apps/api/src/services/alerts/AlertsService.ts | 7 ++- apps/mobile/lib/time-utils.ts | 10 +++++ .../query-builder-timeseries.test.ts | 6 +-- .../api/warehouse/query-builder-timeseries.ts | 10 ++--- .../config/settings-fields.tsx | 3 +- .../query-builder/widget-builder-shared.ts | 13 ++---- packages/domain/src/http/dashboards.ts | 2 + packages/domain/src/query-engine.ts | 9 +++- packages/query-engine/src/datetime.test.ts | 41 ++++++++++++++++++ packages/query-engine/src/datetime.ts | 43 +++++++++++++++++-- .../query-engine/src/runtime/query-engine.ts | 16 +++++-- packages/query-model/src/series-reducer.ts | 10 +++-- .../widgets/src/dashboard/shared/transform.ts | 11 ++++- 15 files changed, 174 insertions(+), 53 deletions(-) diff --git a/apps/api/src/routes/v1/query-engine.http.ts b/apps/api/src/routes/v1/query-engine.http.ts index dceeab780..0a8b155e7 100644 --- a/apps/api/src/routes/v1/query-engine.http.ts +++ b/apps/api/src/routes/v1/query-engine.http.ts @@ -78,6 +78,7 @@ import { WarehouseQueryService } from "@/services/warehouse/WarehouseQueryServic import { traceCacheTtlSeconds } from "@/services/warehouse/trace-detail-cache" import { CH, + computeBucketSeconds, formatWarehouseDateTime, parseWarehouseDateTime, QueryEngineExecuteBatchResponse, @@ -1788,13 +1789,15 @@ export const HttpQueryEngineLive = HttpApiBuilder.group(MapleApi, "queryEngine", }), ) -// Auto-bucket helper for raw-SQL $__interval_s when the user didn't supply -// granularitySeconds. Mirrors apps/web/src/api/tinybird/timeseries-utils.ts so -// the backend can compute it without depending on the web package. - -const TARGET_POINTS = 30 -const AUTO_BUCKET_LADDER = [300, 900, 1800, 3600, 14400, 86400] as const - +/** + * Auto-bucket for raw-SQL `$__interval_s` when the caller didn't supply + * `granularitySeconds`. + * + * Was a private ladder duplicating `computeBucketSeconds`; the only two + * differences were a 30-point target and a 300s floor, both of which the shared + * one now expresses. The floor is load-bearing: a sub-5-minute `$__interval_s` + * produces exactly the scan the granularity was chosen to avoid. + */ function computeAutoBucketSeconds(startTime: string, endTime: string): number { const toEpochMs = (value: string) => new Date(value.replace(" ", "T") + "Z").getTime() const startMs = toEpochMs(startTime) @@ -1802,10 +1805,5 @@ function computeAutoBucketSeconds(startTime: string, endTime: string): number { if (Number.isNaN(startMs) || Number.isNaN(endMs) || endMs <= startMs) { return 300 } - const rangeSeconds = Math.max((endMs - startMs) / 1000, 1) - const raw = Math.ceil(rangeSeconds / TARGET_POINTS) - return AUTO_BUCKET_LADDER.reduce( - (best, candidate) => (Math.abs(candidate - raw) < Math.abs(best - raw) ? candidate : best), - AUTO_BUCKET_LADDER[0], - ) + return computeBucketSeconds(startMs, endMs, { targetPoints: 30, minBucketSeconds: 300 }) } diff --git a/apps/api/src/services/alerts/AlertRuleModel.ts b/apps/api/src/services/alerts/AlertRuleModel.ts index b8d5b3ff9..a828dea3e 100644 --- a/apps/api/src/services/alerts/AlertRuleModel.ts +++ b/apps/api/src/services/alerts/AlertRuleModel.ts @@ -1,4 +1,5 @@ import { + alertWindowBucketSeconds, CompiledAlertQueryPlan, QueryEngineAlertReducer, QueryEngineNoDataBehavior, @@ -17,6 +18,7 @@ import { AlertSignalType as AlertSignalTypeSchema, AlertValidationError, QueryBuilderQueryDraftSchema, + UNGROUPED_GROUP_KEY, UserId, type AlertComparator, type AlertDestinationId, @@ -151,6 +153,24 @@ const planGroupingTokens = ( export const isGroupedPlan = (plan: Schema.Schema.Type): boolean => plan.kind === "raw_sql" || planGroupingTokens(plan) != null +/** + * Translate a group key from the query engine's vocabulary into storage's. + * + * The engine emits a generic `"all"` for an ungrouped result; storage, the wire + * and the UI all spell that `UNGROUPED_GROUP_KEY` (`"__total__"`). The two must + * not be conflated — an `alert_rule_states` row keyed `"all"` is invisible to + * every reader — and the translation was previously open-coded at each of the + * three sites that needed it (scheduler evaluation, preview series, preview's + * empty-range seed), which is exactly the shape of bug that survives review. + * + * `evaluateRule` and `previewRule` are the only boundaries; everything + * downstream of them is already in storage vocabulary. + */ +export const toStorageGroupKey = ( + plan: Schema.Schema.Type, + engineGroupKey: string, +): string => (isGroupedPlan(plan) ? engineGroupKey : UNGROUPED_GROUP_KEY) + export const planEvaluateSource = ( plan: Schema.Schema.Type, windowMinutes: number, @@ -179,7 +199,7 @@ export const compileRulePlan = Effect.fn("AlertsService.compileRulePlan")(functi readonly windowMinutes: number readonly groupBy: AlertGroupBy | null }): Effect.fn.Return, AlertValidationError> { - const bucketSeconds = Math.max(rule.windowMinutes * 60, 60) + const bucketSeconds = alertWindowBucketSeconds(rule.windowMinutes) const envFilter = rule.environments.length > 0 ? { environments: rule.environments } : {} const baseTraceFilters = { ...(rule.serviceName == null ? {} : { serviceName: rule.serviceName }), diff --git a/apps/api/src/services/alerts/AlertsService.ts b/apps/api/src/services/alerts/AlertsService.ts index a2d8f4bf3..3ebb51d83 100644 --- a/apps/api/src/services/alerts/AlertsService.ts +++ b/apps/api/src/services/alerts/AlertsService.ts @@ -101,6 +101,7 @@ import { AlertRulesService, makeAlertRulePersistence, type AlertRulesServiceShap import { compileRulePlan, isGroupedPlan, + toStorageGroupKey, makeAlertValidationError as makeValidationError, planEvaluateSource, serviceNamesFromRow, @@ -522,10 +523,9 @@ export class AlertsService extends Context.Service ({ evaluation: applyEvaluationLogic(rule, obs), - groupKey: grouped ? obs.groupKey : UNGROUPED_GROUP_KEY, + groupKey: toStorageGroupKey(plan, obs.groupKey), })) }) @@ -1117,10 +1117,9 @@ export class AlertsService extends Context.Service 0, diff --git a/apps/mobile/lib/time-utils.ts b/apps/mobile/lib/time-utils.ts index 850edc760..c90399270 100644 --- a/apps/mobile/lib/time-utils.ts +++ b/apps/mobile/lib/time-utils.ts @@ -38,6 +38,16 @@ export function getPreviousTimeRange(shorthand: TimeRangeKey): { startTime: stri } } +// A deliberate copy of `computeBucketSeconds` from `@maple/query-engine`. +// +// `apps/mobile` has no `@maple/*` dependencies at all — it is a standalone React +// Native app that re-declares the wire shapes it needs (see the query-draft +// types in `lib/api.ts`). Taking the workspace dependency to share one pure +// function would pull the query engine's whole module graph into a Metro bundle. +// +// Keep the ladder and target in sync with `packages/query-engine/src/datetime.ts` +// if either changes; this pair matches the 30-point / 300s-floor configuration +// that the raw-SQL `$__interval_s` path also uses. const TARGET_POINTS = 30 const AUTO_BUCKET_LADDER = [300, 900, 1800, 3600, 14400, 86400] as const diff --git a/apps/web/src/api/warehouse/query-builder-timeseries.test.ts b/apps/web/src/api/warehouse/query-builder-timeseries.test.ts index f5984f066..4b6dda3fd 100644 --- a/apps/web/src/api/warehouse/query-builder-timeseries.test.ts +++ b/apps/web/src/api/warehouse/query-builder-timeseries.test.ts @@ -202,9 +202,9 @@ describe("query-builder timeseries strategy", () => { ) it("uses the shared auto bucket ladder", () => { - expect(__testables.computeAutoBucketSeconds("2026-01-01 00:00:00", "2026-01-01 00:30:00")).toBe(60) - expect(__testables.computeAutoBucketSeconds("2026-01-01 00:00:00", "2026-01-01 06:00:00")).toBe(300) - expect(__testables.computeAutoBucketSeconds("2026-01-01 00:00:00", "2026-01-08 00:00:00")).toBe(3600) + expect(__testables.computeBucketSeconds("2026-01-01 00:00:00", "2026-01-01 00:30:00")).toBe(60) + expect(__testables.computeBucketSeconds("2026-01-01 00:00:00", "2026-01-01 06:00:00")).toBe(300) + expect(__testables.computeBucketSeconds("2026-01-01 00:00:00", "2026-01-08 00:00:00")).toBe(3600) }) it("counts only query results with real series data", () => { diff --git a/apps/web/src/api/warehouse/query-builder-timeseries.ts b/apps/web/src/api/warehouse/query-builder-timeseries.ts index ed81f3440..eedfc7a7b 100644 --- a/apps/web/src/api/warehouse/query-builder-timeseries.ts +++ b/apps/web/src/api/warehouse/query-builder-timeseries.ts @@ -115,10 +115,6 @@ interface QueryBuilderTimeseriesResponse { const toEpochMs = parseWarehouseDateTime -function computeAutoBucketSeconds(startTime: string, endTime: string): number { - return computeBucketSeconds(startTime, endTime) -} - function resolveTimeseriesBucketSpec(spec: QuerySpec, startTime: string, endTime: string): QuerySpec { if (spec.kind !== "timeseries" || spec.bucketSeconds) { return spec @@ -126,7 +122,7 @@ function resolveTimeseriesBucketSpec(spec: QuerySpec, startTime: string, endTime return { ...spec, - bucketSeconds: computeAutoBucketSeconds(startTime, endTime), + bucketSeconds: computeBucketSeconds(startTime, endTime), } satisfies QuerySpec } @@ -143,7 +139,7 @@ function resolveExecutionSpecForWindow( return resolved } - const autoBucketSeconds = computeAutoBucketSeconds(window.startTime, window.endTime) + const autoBucketSeconds = computeBucketSeconds(window.startTime, window.endTime) const selectedBucketSeconds = Math.max(resolved.bucketSeconds ?? autoBucketSeconds, autoBucketSeconds) return { ...resolved, @@ -697,7 +693,7 @@ function shiftRunResults(results: QueryRunResult[], shiftMs: number): QueryRunRe } export const __testables = { - computeAutoBucketSeconds, + computeBucketSeconds, resolveTimeseriesBucketSpec, resolveExecutionSpecForWindow, buildExecutionWindows, diff --git a/apps/web/src/components/dashboard-builder/config/settings-fields.tsx b/apps/web/src/components/dashboard-builder/config/settings-fields.tsx index ca04aacfe..089a301c0 100644 --- a/apps/web/src/components/dashboard-builder/config/settings-fields.tsx +++ b/apps/web/src/components/dashboard-builder/config/settings-fields.tsx @@ -14,6 +14,7 @@ import { WidgetBuilderForm } from "@/atoms/widget-query-builder-atoms" import { useAtom } from "@/lib/effect-atom" import { PANEL_TYPES, fromPanelType, toPanelType } from "@/lib/query-builder/panel-types" import { + STAT_AGGREGATES, toSeriesFieldOptions, type QueryBuilderWidgetState, type StatAggregate, @@ -375,8 +376,6 @@ function Legend({ seriesStats = true }: { seriesStats?: boolean }) { ) } -const STAT_AGGREGATES: StatAggregate[] = ["first", "sum", "count", "avg", "max", "min"] - /** How the timeseries a stat or gauge reads is reduced to one number. */ function ScalarReduction() { const { state, seriesFieldOptions, set } = useSettings() diff --git a/apps/web/src/lib/query-builder/widget-builder-shared.ts b/apps/web/src/lib/query-builder/widget-builder-shared.ts index 81179411a..dc8fba258 100644 --- a/apps/web/src/lib/query-builder/widget-builder-shared.ts +++ b/apps/web/src/lib/query-builder/widget-builder-shared.ts @@ -16,6 +16,7 @@ import type { WidgetDataSource, } from "@/components/dashboard-builder/types" import type { LegendPosition } from "@/components/dashboard-builder/config/settings-fields" +import { STAT_AGGREGATES, type StatAggregate } from "@maple/domain/http" import type { HeatmapColorScale, HeatmapScaleType } from "@maple/domain/http" import { normalizeKey, parseBoolean, parseWhereClause as parseWhereClauses } from "@maple/domain/where-clause" @@ -27,7 +28,8 @@ import { normalizeKey, parseBoolean, parseWhereClause as parseWhereClauses } fro // definitions under `components/dashboard-builder/widgets/types/` can import it // without an import cycle through the registry those dispatchers read. -export type StatAggregate = "sum" | "first" | "count" | "avg" | "max" | "min" +// The single widget-side spelling of the shared reducer table. +export { STAT_AGGREGATES, type StatAggregate } from "@maple/domain/http" export interface QueryBuilderWidgetState { visualization: VisualizationType @@ -173,14 +175,7 @@ function toMetricType(input: unknown, fallback: QueryBuilderMetricType): QueryBu } export function toStatAggregate(value: unknown): StatAggregate { - return value === "sum" || - value === "first" || - value === "count" || - value === "avg" || - value === "max" || - value === "min" - ? value - : "first" + return STAT_AGGREGATES.find((candidate) => candidate === value) ?? "first" } function normalizeLoadedQuery(raw: QueryBuilderQueryDraft, index: number): QueryBuilderQueryDraft { diff --git a/packages/domain/src/http/dashboards.ts b/packages/domain/src/http/dashboards.ts index 572ca630a..d413a3d11 100644 --- a/packages/domain/src/http/dashboards.ts +++ b/packages/domain/src/http/dashboards.ts @@ -45,6 +45,8 @@ export { // so Electric hands the browser whatever version it was last written in. migrateToLatest, PortableDashboardDocument, + STAT_AGGREGATES, + type StatAggregate, type TimeRange, TimeRangeSchema, WidgetDataSourceSchema, diff --git a/packages/domain/src/query-engine.ts b/packages/domain/src/query-engine.ts index b7c1026df..62e710a84 100644 --- a/packages/domain/src/query-engine.ts +++ b/packages/domain/src/query-engine.ts @@ -1,4 +1,5 @@ import { Schema } from "effect" +import { ALERT_REDUCERS } from "@maple/query-model" import { PublicHttpErrorBodySchema } from "./http/error-policy" import { CommitSha, @@ -538,7 +539,13 @@ export class QueryEngineExecuteBatchResponse extends Schema.Class diff --git a/packages/query-engine/src/datetime.test.ts b/packages/query-engine/src/datetime.test.ts index 24275b560..7323c82a4 100644 --- a/packages/query-engine/src/datetime.test.ts +++ b/packages/query-engine/src/datetime.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest" import { bucketTimeline, cacheSnapSecondsForRange, + alertWindowBucketSeconds, computeBucketSeconds, formatWarehouseDateTime, formatWarehouseDateTimeMs, @@ -107,6 +108,46 @@ describe("computeBucketSeconds", () => { it("honors an explicit targetPoints (denser histograms)", () => { expect(computeBucketSeconds(0, 3600_000, { targetPoints: 60 })).toBe(60) }) + + describe("minBucketSeconds", () => { + const rawSql = { targetPoints: 30, minBucketSeconds: 300 } as const + + it("never returns a rung below the floor, even on a tiny window", () => { + // A sub-5-minute `$__interval_s` produces exactly the scan the raw-SQL + // granularity was chosen to avoid, so the floor holds regardless of how + // short the window is or how far `minBuckets` would otherwise step down. + expect(computeBucketSeconds(0, 30_000, rawSql)).toBe(300) + expect(computeBucketSeconds(0, 600_000, rawSql)).toBe(300) + expect(computeBucketSeconds(0, 0, rawSql)).toBe(300) + }) + + it("reproduces the ladder the raw-SQL path used before it was shared", () => { + // The deleted private ladder was [300, 900, 1800, 3600, 14400, 86400] at + // a 30-point target with no minBuckets clamp. These are its outputs. + const hour = 3600_000 + expect(computeBucketSeconds(0, 0.5 * hour, rawSql)).toBe(300) + expect(computeBucketSeconds(0, 6 * hour, rawSql)).toBe(900) + expect(computeBucketSeconds(0, 24 * hour, rawSql)).toBe(3600) + expect(computeBucketSeconds(0, 7 * 24 * hour, rawSql)).toBe(14400) + expect(computeBucketSeconds(0, 30 * 24 * hour, rawSql)).toBe(86400) + }) + + it("leaves the default (unfloored) ladder alone", () => { + expect(computeBucketSeconds(0, 3600_000)).toBe(computeBucketSeconds(0, 3600_000, {})) + }) + }) +}) + +describe("alertWindowBucketSeconds", () => { + it("makes the evaluation window the bucket", () => { + expect(alertWindowBucketSeconds(5)).toBe(300) + expect(alertWindowBucketSeconds(60)).toBe(3600) + }) + + it("floors at 60s — a zero-width bucket is not a bucket", () => { + expect(alertWindowBucketSeconds(0)).toBe(60) + expect(alertWindowBucketSeconds(-1)).toBe(60) + }) }) describe("bucketTimeline", () => { diff --git a/packages/query-engine/src/datetime.ts b/packages/query-engine/src/datetime.ts index f136e47a4..c40590f96 100644 --- a/packages/query-engine/src/datetime.ts +++ b/packages/query-engine/src/datetime.ts @@ -290,6 +290,18 @@ export interface ComputeBucketSecondsOptions { * near-empty charts on short windows. */ minBuckets?: number + /** + * Drop every ladder rung below this before picking. Default 60 (the whole + * ladder). + * + * This is what a caller with a coarser floor needs: raw-SQL `$__interval_s` + * wants 300, because a sub-5-minute bucket there produces a scan the + * granularity was chosen to avoid. Expressed as a ladder filter rather than a + * post-hoc `Math.max` on purpose — clamping after the fact would round 120 up + * to 300 while leaving the "nearest rung" choice computed against rungs the + * caller cannot use. + */ + minBucketSeconds?: number } /** @@ -305,24 +317,47 @@ export function computeBucketSeconds( ): number { const targetPoints = options?.targetPoints ?? 100 const minBuckets = options?.minBuckets ?? 6 + const minBucketSeconds = options?.minBucketSeconds ?? 0 const rangeSeconds = Math.max((endMs - startMs) / 1000, 1) const raw = Math.max(Math.ceil(rangeSeconds / targetPoints), 1) - let bucket: number = AUTO_BUCKET_LADDER.reduce( + const ladder = AUTO_BUCKET_LADDER.filter((candidate) => candidate >= minBucketSeconds) + const rungs = ladder.length > 0 ? ladder : [AUTO_BUCKET_LADDER[AUTO_BUCKET_LADDER.length - 1]] + + let bucket: number = rungs.reduce( (best, candidate) => (Math.abs(candidate - raw) < Math.abs(best - raw) ? candidate : best), - AUTO_BUCKET_LADDER[0], + rungs[0], ) // Never coarser than what keeps at least `minBuckets` buckets over the range. const maxBucketForMin = Math.floor(rangeSeconds / minBuckets) if (bucket > maxBucketForMin) { - const finer = AUTO_BUCKET_LADDER.filter((candidate) => candidate <= maxBucketForMin) - bucket = finer.length > 0 ? finer[finer.length - 1] : AUTO_BUCKET_LADDER[0] + const finer = rungs.filter((candidate) => candidate <= maxBucketForMin) + bucket = finer.length > 0 ? finer[finer.length - 1] : rungs[0] + } + // `minBuckets` may have stepped below the caller's floor on a short window. + if (bucket < minBucketSeconds) { + bucket = rungs[0] } return bucket } +/** + * Bucket width for an alert rule's evaluation window. + * + * A rule compares one value per window against a threshold, so the bucket IS the + * window — not a fraction of it. Floored at 60s because sub-minute alert windows + * are not offered and a zero-width bucket is not a bucket. + * + * Named rather than inlined because it was previously spelled out at two sites + * (`compileRulePlan`, which bakes it into the stored spec, and + * `prepareAlertEvaluation`'s raw-SQL branch), and a rule whose stored spec + * disagreed with its evaluation-time bucket would silently evaluate a different + * window than the one it was saved with. + */ +export const alertWindowBucketSeconds = (windowMinutes: number): number => Math.max(windowMinutes * 60, 60) + const floorToBucketMs = (epochMs: number, bucketSeconds: number): number => { const bucketMs = bucketSeconds * 1000 return Math.floor(epochMs / bucketMs) * bucketMs diff --git a/packages/query-engine/src/runtime/query-engine.ts b/packages/query-engine/src/runtime/query-engine.ts index 4893751b8..964368062 100644 --- a/packages/query-engine/src/runtime/query-engine.ts +++ b/packages/query-engine/src/runtime/query-engine.ts @@ -29,7 +29,12 @@ import type { OrgId } from "@maple/domain" import { Array as Arr, Duration, Effect, Match, Option, Result, Schema } from "effect" import type { QueryProfileName, SqlQueryOptions, WarehouseQuerySettings } from "../profiles" import { canonicalJSON } from "../canonical-json" -import { computeBucketSeconds, formatWarehouseDateTime, parseWarehouseDateTime } from "../datetime" +import { + alertWindowBucketSeconds, + computeBucketSeconds, + formatWarehouseDateTime, + parseWarehouseDateTime, +} from "../datetime" import { MAX_BREAKDOWN_RANGE_SECONDS, MAX_LIST_RANGE_SECONDS, @@ -60,7 +65,7 @@ export { // Re-exported so `@maple/query-engine/runtime` consumers (apps/api) keep importing // `computeBucketSeconds` from here; the implementation now lives in the pure // `../datetime` module so the web app and the engine share one definition. -export { computeBucketSeconds } from "../datetime" +export { alertWindowBucketSeconds, computeBucketSeconds } from "../datetime" // Same arrangement for the range ceilings: they now live in the pure `../limits` // module so the MCP tools, the v2 API, and the web widget layer all bound @@ -2308,8 +2313,11 @@ const prepareAlertEvaluation = Effect.fnUntraced(function* (request: AlertEvalua if (request.source.kind === "raw_sql") { // One evaluation window is one bucket; a query using `$__timeGroup` lines - // its rows up on exactly that grid. - return Math.max(request.source.windowMinutes * 60, 60) + // its rows up on exactly that grid. Shared with `compileRulePlan`, which + // bakes the same width into a spec-backed rule's stored query — the two + // disagreeing would evaluate a different window than the rule was saved + // with. + return alertWindowBucketSeconds(request.source.windowMinutes) } const query = request.source.query diff --git a/packages/query-model/src/series-reducer.ts b/packages/query-model/src/series-reducer.ts index 51f41626e..d1ed881bb 100644 --- a/packages/query-model/src/series-reducer.ts +++ b/packages/query-model/src/series-reducer.ts @@ -12,15 +12,19 @@ * that has no counterpart fails to compile rather than falling through at * runtime. */ +/** + * Order is user-visible: the widget spelling drives the Aggregate picker, and + * `"first"` leads because it is the runtime default for an absent aggregate. + */ const REDUCER_TABLE = [ { series: "first", alert: "identity" }, { series: "sum", alert: "sum" }, - { series: "avg", alert: "avg" }, - { series: "max", alert: "max" }, - { series: "min", alert: "min" }, // Widget-only: there is no alert reducer that counts buckets, because a rule // compares a value against a threshold rather than a cardinality. { series: "count", alert: null }, + { series: "avg", alert: "avg" }, + { series: "max", alert: "max" }, + { series: "min", alert: "min" }, ] as const satisfies ReadonlyArray<{ series: string; alert: string | null }> export type SeriesReducer = (typeof REDUCER_TABLE)[number]["series"] diff --git a/packages/widgets/src/dashboard/shared/transform.ts b/packages/widgets/src/dashboard/shared/transform.ts index ffd45de0b..548a99469 100644 --- a/packages/widgets/src/dashboard/shared/transform.ts +++ b/packages/widgets/src/dashboard/shared/transform.ts @@ -1,4 +1,5 @@ import { Schema } from "effect" +import { SERIES_REDUCERS, type SeriesReducer } from "@maple/query-model" export const StringRecord = Schema.Record(Schema.String, Schema.String) export const UnknownRecord = Schema.Record(Schema.String, Schema.Unknown) @@ -9,9 +10,15 @@ export const UnknownRecord = Schema.Record(Schema.String, Schema.Unknown) * Closed in schema v2 and open (`Schema.String`) in v1. The set is exactly what * `applyTransform` implements and what the Aggregate picker offers; `"first"` * leads because it is the runtime default for an absent aggregate. + * + * The widget spelling of the shared reducer table in `@maple/query-model` — the + * alert-rule spelling of the same concept is `ALERT_REDUCERS` there. The two + * literal sets stay distinct (widgets persist `"first"`, rules persist + * `"identity"`), but they derive from one table so a reducer added to one is + * forced to declare whether it has a counterpart in the other. */ -export const STAT_AGGREGATES = ["first", "sum", "count", "avg", "max", "min"] as const -export type StatAggregate = (typeof STAT_AGGREGATES)[number] +export const STAT_AGGREGATES = SERIES_REDUCERS +export type StatAggregate = SeriesReducer /** * `applyTransform` compares `direction === "desc"` and sorts ascending for From c74e39e891b9ef314fcb13561eb75ac358bb4c04 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Thu, 13 Aug 2026 19:12:57 +0200 Subject: [PATCH 03/13] refactor(widgets): add version-agnostic data-source accessors, still on v2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Groundwork for the v3 data-source union. Every backend consumer that reached into `dataSource.endpoint` / `dataSource.params` by hand now goes through accessors that read v2 AND v3 identically, so the version flip becomes a small diff instead of a wide one. `access.test.ts` states both shapes per case and asserts one result, so a drift between them fails there rather than at cutover. `dataSourceEndpoint` deliberately returns null for a typed v3 arm instead of synthesising `"custom_query_builder_timeseries"` — inventing a legacy name would quietly re-create the endpoint-string sniffing the union exists to remove. The one place a legacy name is still produced is the `inspect_chart_data` response label, which is a published MCP contract; it is named and isolated. Two fixes found on the way: `migrateToLatest` restamped a document from a NEWER build downward. Unknown versions read as 1, so a rollback would run a v3 document through the whole chain as though it were the oldest shape and then stamp it current. Decode fails either way, but stamped, the next writer persists the lie and the original version is gone. Failing to read is recoverable; corrupting is not. Now returned untouched. Wrote `interpolation-keys.test.ts` — the guard that `display.ts` has been citing by name for the load-bearing `listWhereClause` field, which did not exist. Interpolation picks its formatting by KEY NAME, so renaming that field compiles, passes every schema, and silently changes behaviour. The test pins the real failure shape: a renamed key stops dropping an All-selected clause and expands it to `environment = prd,stg` — a filter matching nothing, where the user asked for no filter at all. 41/41 typecheck; 91 widgets, 224 api mcp/dashboard, 39 query-engine tests pass. --- apps/api/src/mcp/lib/inspect-widget.ts | 74 +++++---- apps/api/src/mcp/tools/inspect-chart-data.ts | 26 +-- .../interpolation-keys.test.ts | 90 ++++++++++ packages/widgets/src/dashboard/access.test.ts | 156 ++++++++++++++++++ packages/widgets/src/dashboard/access.ts | 131 +++++++++++++++ packages/widgets/src/dashboard/index.ts | 8 + .../widgets/src/dashboard/migrations/index.ts | 16 ++ .../dashboard/migrations/migrations.test.ts | 21 +++ 8 files changed, 473 insertions(+), 49 deletions(-) create mode 100644 packages/query-engine/src/dashboard-variables/interpolation-keys.test.ts create mode 100644 packages/widgets/src/dashboard/access.test.ts create mode 100644 packages/widgets/src/dashboard/access.ts diff --git a/apps/api/src/mcp/lib/inspect-widget.ts b/apps/api/src/mcp/lib/inspect-widget.ts index bb4e0c37e..ac39085f5 100644 --- a/apps/api/src/mcp/lib/inspect-widget.ts +++ b/apps/api/src/mcp/lib/inspect-widget.ts @@ -35,11 +35,32 @@ import type { WidgetInspectionSummary, WidgetInspectionVerdict, } from "@maple/domain" +import { dataSourceEndpoint, dataSourceQuerySet, dataSourceRawSql } from "@maple/widgets/dashboard" import type { TenantContext } from "@/services/auth/tenant-context" -const TIMESERIES_ENDPOINT = "custom_query_builder_timeseries" -const BREAKDOWN_ENDPOINT = "custom_query_builder_breakdown" +/** + * The label a raw-SQL inspection reports, NOT a dispatch key — dispatch goes + * through `dataSourceRawSql`, which reads v2 and v3 alike. This name is part of + * the MCP response contract, so it stays spelled the v2 way even once the stored + * data source no longer has an endpoint. + */ const RAW_SQL_ENDPOINT = "raw_sql_chart" + +/** + * The legacy endpoint name for a query result shape. + * + * `inspect_chart_data` reports `endpoint` as part of its response contract, but + * a v3 `kind: "query"` data source has no endpoint — the shape is the identity. + * Synthesising the v2 name here keeps the MCP payload stable for agents that + * already branch on it. This is the ONLY place a legacy endpoint name is + * produced from a typed data source; dispatch never goes through it. + */ +const QUERY_SHAPE_ENDPOINTS = { + timeseries: "custom_query_builder_timeseries", + breakdown: "custom_query_builder_breakdown", + list: "custom_query_builder_list", +} as const + const MAX_QUERIES = 5 // Rows captured for a raw-SQL widget inspection — enough to spot-check, capped // so a wide/long result doesn't bloat the response. @@ -73,15 +94,13 @@ const decodeQuerySpec = Schema.decodeUnknownEffect(QuerySpec) export const collectBlockingBuilderWarnings = Effect.fn("collectBlockingBuilderWarnings")(function* ( dataSource: DashboardWidget["dataSource"], ) { - const endpoint = dataSource.endpoint - const isTimeseries = endpoint === TIMESERIES_ENDPOINT - const isBreakdown = endpoint === BREAKDOWN_ENDPOINT + const querySet = dataSourceQuerySet(dataSource) + if (querySet === null) return [] as string[] + const isTimeseries = querySet.resultShape === "timeseries" + const isBreakdown = querySet.resultShape === "breakdown" if (!isTimeseries && !isBreakdown) return [] as string[] - const rawParams = dataSource.params - if (!rawParams || typeof rawParams !== "object") return [] as string[] - - const decoded = yield* Effect.result(decodeQueryBuilderParams(rawParams)) + const decoded = yield* Effect.result(decodeQueryBuilderParams({ queries: querySet.queries })) if (Result.isFailure(decoded)) return [] as string[] const drafts = decoded.success.queries.filter((q) => q.enabled !== false) @@ -320,10 +339,10 @@ const inspectRawSqlWidget = Effect.fn("inspectRawSqlWidget")(function* ( widget: DashboardWidget, timeRange: InspectWidgetTimeRange, ) { - const rawParams = widget.dataSource.params as Record | undefined - const sql = typeof rawParams?.sql === "string" ? rawParams.sql : undefined + const rawSql = dataSourceRawSql(widget.dataSource) + const sql = rawSql !== null && rawSql.sql.length > 0 ? rawSql.sql : undefined - if (!sql) { + if (rawSql === null || !sql) { return { kind: "skipped", reason: "no_params", @@ -332,9 +351,7 @@ const inspectRawSqlWidget = Effect.fn("inspectRawSqlWidget")(function* ( } const granularitySeconds = - typeof rawParams?.granularitySeconds === "number" - ? rawParams.granularitySeconds - : autoBucketSeconds(timeRange.startTime, timeRange.endTime) + rawSql.granularitySeconds ?? autoBucketSeconds(timeRange.startTime, timeRange.endTime) const result = yield* runRawSql({ tenant, @@ -393,28 +410,24 @@ export const inspectWidget = Effect.fn("inspectWidget")( function* (input: InspectWidgetInput) { const { tenant, widget, timeRange } = input - const endpoint = widget.dataSource.endpoint - const isTimeseries = endpoint === TIMESERIES_ENDPOINT - const isBreakdown = endpoint === BREAKDOWN_ENDPOINT - - if (endpoint === RAW_SQL_ENDPOINT) { + if (dataSourceRawSql(widget.dataSource) !== null) { return yield* inspectRawSqlWidget(tenant, widget, timeRange) } - if (!isTimeseries && !isBreakdown) { - return { kind: "unsupported", endpoint } satisfies InspectionOutcome - } + const querySet = dataSourceQuerySet(widget.dataSource) + const isTimeseries = querySet?.resultShape === "timeseries" + const isBreakdown = querySet?.resultShape === "breakdown" - const rawParams = widget.dataSource.params - if (!rawParams || typeof rawParams !== "object") { + if (querySet === null || (!isTimeseries && !isBreakdown)) { return { - kind: "skipped", - reason: "no_params", - detail: "Widget has no dataSource.params; cannot inspect.", + kind: "unsupported", + endpoint: dataSourceEndpoint(widget.dataSource) ?? "unknown", } satisfies InspectionOutcome } - const decodedParamsResult = yield* Effect.result(decodeQueryBuilderParams(rawParams)) + const decodedParamsResult = yield* Effect.result( + decodeQueryBuilderParams({ queries: querySet.queries, formulas: querySet.formulas }), + ) if (Result.isFailure(decodedParamsResult)) { return { kind: "skipped", @@ -693,7 +706,8 @@ export const inspectWidget = Effect.fn("inspectWidget")( id: widget.id, ...(widget.display.title !== undefined && { title: widget.display.title }), visualization: widget.visualization, - endpoint, + endpoint: + dataSourceEndpoint(widget.dataSource) ?? QUERY_SHAPE_ENDPOINTS[querySet.resultShape], ...(widget.display.unit !== undefined && { displayUnit: widget.display.unit }), // True only when formulas are present but NOT evaluated (non-timeseries // widgets). Timeseries formulas are now evaluated and appear as their diff --git a/apps/api/src/mcp/tools/inspect-chart-data.ts b/apps/api/src/mcp/tools/inspect-chart-data.ts index f9dd409a5..3e6fa2efa 100644 --- a/apps/api/src/mcp/tools/inspect-chart-data.ts +++ b/apps/api/src/mcp/tools/inspect-chart-data.ts @@ -6,6 +6,7 @@ import { type McpToolResult, } from "./types" import { Effect, Schema } from "effect" +import { dataSourceEndpoint } from "@maple/widgets/dashboard" import { CurrentMcpTenant } from "@/mcp/lib/query-warehouse" import { DashboardPersistenceService } from "@/services/dashboards/DashboardPersistenceService" import { createDualContent } from "@/mcp/lib/structured-output" @@ -82,21 +83,16 @@ function unsupportedEndpointResult( `## Widget inspection: ${widget.display.title ?? widget.id}`, `Dashboard: ${dashboardName}`, `Visualization: ${widget.visualization}`, - `Endpoint: ${widget.dataSource.endpoint}`, + `Endpoint: ${dataSourceEndpoint(widget.dataSource) ?? "(typed data source)"}`, ``, `This endpoint is not yet supported by inspect_chart_data.`, `Use the \`query_data\` tool directly to verify, with the params shown below.`, ``, `Widget definition:`, - JSON.stringify( - { - endpoint: widget.dataSource.endpoint, - params: widget.dataSource.params, - transform: widget.dataSource.transform, - }, - null, - 2, - ), + // The whole data source rather than three hand-picked fields: this is the + // diagnostic path for a widget nothing else could read, so showing exactly + // what is stored beats showing the subset an older shape happened to have. + JSON.stringify(widget.dataSource, null, 2), ].join("\n") return { content: [{ type: "text" as const, text }] } @@ -283,15 +279,7 @@ export function registerInspectChartDataTool(server: McpToolRegistrar) { { id: widget.id, visualization: widget.visualization, - dataSource: { - endpoint: widget.dataSource.endpoint, - ...(widget.dataSource.params && { - params: widget.dataSource.params as Record, - }), - ...(widget.dataSource.transform && { - transform: widget.dataSource.transform as Record, - }), - }, + dataSource: widget.dataSource, display: { ...(widget.display.title !== undefined && { title: widget.display.title }), ...(widget.display.unit !== undefined && { unit: widget.display.unit }), diff --git a/packages/query-engine/src/dashboard-variables/interpolation-keys.test.ts b/packages/query-engine/src/dashboard-variables/interpolation-keys.test.ts new file mode 100644 index 000000000..486923c7c --- /dev/null +++ b/packages/query-engine/src/dashboard-variables/interpolation-keys.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, it } from "vitest" +import { interpolateWidgetParams, type VariableValues } from "./interpolate" + +/** + * The guard `packages/widgets/src/dashboard/shared/display.ts` claims exists. + * + * Variable interpolation selects its formatting by KEY NAME, not by type: a key + * ending in `whereclause` gets clause-aware treatment (an "All" selection drops + * the whole clause), a key named exactly `sql` gets escaped ClickHouse literals, + * everything else gets plain text. That makes the field names below part of the + * contract — renaming `listWhereClause` to `listFilter` does not fail to + * compile, does not fail any schema, and silently sends a literal `$service` to + * the warehouse. + * + * These tests exist to fail loudly when a rename happens. If you are here + * because one broke: the rename is fine, but the matching rule in + * `interpolate.ts` has to move with it. + */ + +const values: VariableValues = { + service: { value: "api", isAll: false, options: ["api", "web"] }, + env: { value: "$__all", isAll: true, options: ["prd", "stg"] }, +} + +describe("interpolation key contract", () => { + it.each([ + ["whereClause", "queries[i].whereClause"], + ["listWhereClause", "display.listWhereClause"], + ["WHERECLAUSE", "any casing"], + ])("treats %s as a where-clause (drops All-selected clauses)", (key) => { + const out = interpolateWidgetParams( + { [key]: "service.name = $service AND environment = $env" }, + values, + ) + // `$env` is All, so its clause is removed entirely rather than substituted: + // "All" means "do not filter on this", not "match the literal string". + // The surviving value is substituted UNQUOTED — a where-clause is parsed by + // the engine's own grammar, not emitted as SQL. Only `sql` quotes. + expect(out[key]).toBe("service.name = api") + }) + + it("treats `sql` as SQL and escapes the substituted literal", () => { + const out = interpolateWidgetParams({ sql: "WHERE service = $service" }, values) + expect(out.sql).toBe("WHERE service = 'api'") + }) + + it("quotes a value that would otherwise break out of its SQL literal", () => { + const out = interpolateWidgetParams( + { sql: "WHERE service = $service" }, + { service: { value: "a' OR 1=1 --", isAll: false, options: [] } }, + ) + expect(out.sql).toBe("WHERE service = 'a\\' OR 1=1 --'") + }) + + it("finds a where-clause key at any depth", () => { + const out = interpolateWidgetParams( + { queries: [{ id: "a", whereClause: "service.name = $service" }] }, + values, + ) + expect(out).toEqual({ queries: [{ id: "a", whereClause: "service.name = api" }] }) + }) + + it("uses plain substitution for a key that is NOT a recognised clause name", () => { + // The negative half of the contract, and the reason the name is + // load-bearing. Same input as the where-clause cases above: a renamed field + // stops dropping the All-selected clause and instead expands it to a + // comma-joined option list, producing `environment = prd,stg` — a filter + // that matches nothing, where the user asked for no filter at all. + const clause = "service.name = $service AND environment = $env" + const renamed = interpolateWidgetParams({ listFilter: clause }, values) + expect(renamed.listFilter).toBe("service.name = api AND environment = prd,stg") + + const guarded = interpolateWidgetParams({ listWhereClause: clause }, values) + expect(guarded.listWhereClause).toBe("service.name = api") + }) + + it("leaves `$__` macros alone", () => { + const out = interpolateWidgetParams( + { sql: "SELECT $__timeGroup(Timestamp) WHERE $__orgFilter AND s = $service" }, + values, + ) + expect(out.sql).toBe("SELECT $__timeGroup(Timestamp) WHERE $__orgFilter AND s = 'api'") + }) + + it("keeps an unknown variable reference literal", () => { + // Substituting nothing would silently widen the query to match everything. + const out = interpolateWidgetParams({ whereClause: "a = $nope" }, values) + expect(out.whereClause).toBe("a = $nope") + }) +}) diff --git a/packages/widgets/src/dashboard/access.test.ts b/packages/widgets/src/dashboard/access.test.ts new file mode 100644 index 000000000..021e4b11c --- /dev/null +++ b/packages/widgets/src/dashboard/access.test.ts @@ -0,0 +1,156 @@ +import { describe, expect, it } from "vitest" +import { + dataSourceEndpoint, + dataSourceQuerySet, + dataSourceRawSql, + dataSourceRouteParams, + isQueryDataSource, +} from "./access" + +/** + * The accessors' whole job is that a v2 data source and the v3 one it migrates + * to read IDENTICALLY. Each case below states both shapes and asserts one + * result, so a drift between them fails here rather than at the version flip. + */ + +const draft = { id: "q1", name: "A", aggregation: "count", dataSource: "traces" } +const formula = { id: "f1", name: "F1", expression: "A / B", legend: "ratio" } + +describe("dataSourceQuerySet", () => { + it("reads a timeseries query set from either version", () => { + const v2 = { + endpoint: "custom_query_builder_timeseries", + params: { queries: [draft], formulas: [formula], comparison: { mode: "previous_period" } }, + } + const v3 = { + kind: "query", + resultShape: "timeseries", + queries: [draft], + formulas: [formula], + comparison: { mode: "previous_period" }, + } + const expected = { + resultShape: "timeseries", + queries: [draft], + formulas: [formula], + comparison: { mode: "previous_period" }, + } + expect(dataSourceQuerySet(v2)).toEqual(expected) + expect(dataSourceQuerySet(v3)).toEqual(expected) + }) + + it("derives the result shape from the endpoint on v2", () => { + expect(dataSourceQuerySet({ endpoint: "custom_query_builder_breakdown" })?.resultShape).toBe( + "breakdown", + ) + expect(dataSourceQuerySet({ endpoint: "custom_query_builder_list" })?.resultShape).toBe("list") + }) + + it("returns null for a fixed-route endpoint", () => { + expect(dataSourceQuerySet({ endpoint: "service_overview", params: { x: 1 } })).toBeNull() + expect(dataSourceQuerySet({ kind: "route", endpoint: "service_overview" })).toBeNull() + }) + + it("returns null for raw SQL, markdown and junk", () => { + expect(dataSourceQuerySet({ endpoint: "raw_sql_chart" })).toBeNull() + expect(dataSourceQuerySet({ kind: "raw_sql", sql: "SELECT 1" })).toBeNull() + expect(dataSourceQuerySet({ kind: "static" })).toBeNull() + expect(dataSourceQuerySet(null)).toBeNull() + expect(dataSourceQuerySet("nope")).toBeNull() + }) + + it("reports what is stored rather than what would decode", () => { + // A read accessor, not a validator. The MCP inspector and the template + // checker both want to see the malformed draft in order to report on it; + // silently dropping it would make a broken widget look empty. + const malformed = { id: "q1" } + expect( + dataSourceQuerySet({ kind: "query", resultShape: "list", queries: [malformed] })?.queries, + ).toEqual([malformed]) + }) + + it("treats absent or non-array queries as empty, not as a failure", () => { + expect(dataSourceQuerySet({ endpoint: "custom_query_builder_timeseries" })?.queries).toEqual([]) + expect( + dataSourceQuerySet({ endpoint: "custom_query_builder_timeseries", params: { queries: "x" } }) + ?.queries, + ).toEqual([]) + }) +}) + +describe("dataSourceRawSql", () => { + it("reads the payload from either version", () => { + const expected = { sql: "SELECT 1", displayType: "line", granularitySeconds: 60 } + expect( + dataSourceRawSql({ + endpoint: "raw_sql_chart", + params: { sql: "SELECT 1", displayType: "line", granularitySeconds: 60 }, + }), + ).toEqual(expected) + expect( + dataSourceRawSql({ + kind: "raw_sql", + sql: "SELECT 1", + displayType: "line", + granularitySeconds: 60, + }), + ).toEqual(expected) + }) + + it("returns an empty string for a widget saved before any SQL was written", () => { + // Representable and meaningful — callers warn about it. Null would make it + // indistinguishable from "this isn't a raw-SQL widget". + expect(dataSourceRawSql({ endpoint: "raw_sql_chart", params: {} })?.sql).toBe("") + expect(dataSourceRawSql({ kind: "raw_sql" })?.sql).toBe("") + }) + + it("returns null for anything that is not raw SQL", () => { + expect(dataSourceRawSql({ endpoint: "custom_query_builder_timeseries" })).toBeNull() + expect(dataSourceRawSql({ kind: "query", resultShape: "timeseries", queries: [] })).toBeNull() + }) +}) + +describe("dataSourceEndpoint", () => { + it("reads the endpoint on v2 and on a v3 route", () => { + expect(dataSourceEndpoint({ endpoint: "service_overview" })).toBe("service_overview") + expect(dataSourceEndpoint({ kind: "route", endpoint: "service_overview" })).toBe("service_overview") + }) + + it("returns null for a typed v3 arm rather than inventing a legacy name", () => { + // Synthesising "custom_query_builder_timeseries" here would re-create the + // endpoint-string sniffing the typed union exists to remove. + expect(dataSourceEndpoint({ kind: "query", resultShape: "timeseries", queries: [] })).toBeNull() + expect(dataSourceEndpoint({ kind: "raw_sql", sql: "" })).toBeNull() + expect(dataSourceEndpoint({ kind: "static" })).toBeNull() + }) +}) + +describe("dataSourceRouteParams", () => { + it("reads the bag for a curated route on either version", () => { + expect(dataSourceRouteParams({ endpoint: "service_overview", params: { limit: 5 } })).toEqual({ + limit: 5, + }) + expect( + dataSourceRouteParams({ kind: "route", endpoint: "service_overview", params: { limit: 5 } }), + ).toEqual({ limit: 5 }) + }) + + it("returns undefined for a typed v3 arm — nothing opaque is left", () => { + expect(dataSourceRouteParams({ kind: "query", resultShape: "list", queries: [] })).toBeUndefined() + expect(dataSourceRouteParams({ kind: "raw_sql", sql: "SELECT 1" })).toBeUndefined() + }) +}) + +describe("isQueryDataSource", () => { + it("agrees with dataSourceQuerySet across both versions", () => { + for (const source of [ + { endpoint: "custom_query_builder_timeseries", params: { queries: [draft] } }, + { kind: "query", resultShape: "timeseries", queries: [draft] }, + { endpoint: "service_overview" }, + { kind: "raw_sql", sql: "SELECT 1" }, + null, + ]) { + expect(isQueryDataSource(source)).toBe(dataSourceQuerySet(source) !== null) + } + }) +}) diff --git a/packages/widgets/src/dashboard/access.ts b/packages/widgets/src/dashboard/access.ts new file mode 100644 index 000000000..7786b24be --- /dev/null +++ b/packages/widgets/src/dashboard/access.ts @@ -0,0 +1,131 @@ +import type { QuerySet, QueryResultShape } from "@maple/query-model" + +/** + * Reading a widget's data source without caring which schema version wrote it. + * + * A v2 data source is `{ endpoint, params }` — an opaque bag naming a web-side + * server function. A v3 one is a discriminated union whose query-carrying arms + * are typed. Every accessor here reads BOTH, which is what lets the ~dozen + * backend consumers (MCP tools, templates, the Perses importer, variable + * interpolation) move off the raw shape one commit at a time, while v2 is still + * the stored version. By the time the version flips there is nothing left + * reaching into `params` by hand. + * + * Deliberately `unknown`-in: callers hold widgets typed by whichever schema + * version their module imported, and a parameter typed to one of them would + * just push a cast to every call site. The narrowing happens once, here. + */ + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value) + +/** The 5 v2 endpoints that carried a user-authored query rather than a fixed route. */ +const QUERY_ENDPOINT_SHAPES: Record = { + custom_query_builder_timeseries: "timeseries", + custom_query_builder_breakdown: "breakdown", + custom_query_builder_list: "list", +} + +const RAW_SQL_ENDPOINT = "raw_sql_chart" + +/** + * The endpoint name, for consumers that still dispatch on it. + * + * Returns null for a v3 data source that is not a `route` — a typed `query` arm + * has no endpoint, and inventing one (`"custom_query_builder_timeseries"`) would + * quietly re-create the string-sniffing this refactor removes. + */ +export const dataSourceEndpoint = (dataSource: unknown): string | null => { + if (!isRecord(dataSource)) return null + if (typeof dataSource.kind === "string") { + return dataSource.kind === "route" && typeof dataSource.endpoint === "string" + ? dataSource.endpoint + : null + } + return typeof dataSource.endpoint === "string" ? dataSource.endpoint : null +} + +/** True when this data source carries a user-authored query set. */ +export const isQueryDataSource = (dataSource: unknown): boolean => dataSourceQuerySet(dataSource) !== null + +/** + * The query set, whichever way it is stored. + * + * Structural on v3; on v2 it reads `params.queries` for the three query-builder + * endpoints. NOT validated — this is a read accessor, and a caller that needs + * decoded drafts should decode. Returning a partly-malformed set is correct + * here: the MCP inspector and the template checker both want to report on what + * is actually stored, not on what would survive a decode. + */ +export const dataSourceQuerySet = ( + dataSource: unknown, +): (QuerySet & { resultShape: QueryResultShape }) | null => { + if (!isRecord(dataSource)) return null + + if (typeof dataSource.kind === "string") { + if (dataSource.kind !== "query") return null + const shape = dataSource.resultShape + return { + resultShape: typeof shape === "string" ? (shape as QueryResultShape) : "timeseries", + queries: Array.isArray(dataSource.queries) ? (dataSource.queries as QuerySet["queries"]) : [], + formulas: Array.isArray(dataSource.formulas) + ? (dataSource.formulas as QuerySet["formulas"]) + : undefined, + comparison: isRecord(dataSource.comparison) + ? (dataSource.comparison as QuerySet["comparison"]) + : undefined, + } + } + + const endpoint = dataSource.endpoint + if (typeof endpoint !== "string") return null + const resultShape = QUERY_ENDPOINT_SHAPES[endpoint] + if (resultShape === undefined) return null + + const params = isRecord(dataSource.params) ? dataSource.params : {} + return { + resultShape, + queries: Array.isArray(params.queries) ? (params.queries as QuerySet["queries"]) : [], + formulas: Array.isArray(params.formulas) ? (params.formulas as QuerySet["formulas"]) : undefined, + comparison: isRecord(params.comparison) ? (params.comparison as QuerySet["comparison"]) : undefined, + } +} + +export interface RawSqlDataSource { + sql: string + displayType?: string + granularitySeconds?: number +} + +/** The raw-SQL payload, from `params` on v2 and the variant's own fields on v3. */ +export const dataSourceRawSql = (dataSource: unknown): RawSqlDataSource | null => { + if (!isRecord(dataSource)) return null + + const source = (() => { + if (typeof dataSource.kind === "string") { + return dataSource.kind === "raw_sql" ? dataSource : null + } + if (dataSource.endpoint !== RAW_SQL_ENDPOINT) return null + return isRecord(dataSource.params) ? dataSource.params : {} + })() + if (source === null) return null + + return { + // An empty string is representable and meaningful — a raw-SQL widget saved + // before any SQL was written. Callers warn about it; they don't get null. + sql: typeof source.sql === "string" ? source.sql : "", + displayType: typeof source.displayType === "string" ? source.displayType : undefined, + granularitySeconds: + typeof source.granularitySeconds === "number" ? source.granularitySeconds : undefined, + } +} + +/** + * The opaque params bag, for the curated fixed-route endpoints that still have + * one. Returns undefined for a typed v3 arm — there is nothing opaque left. + */ +export const dataSourceRouteParams = (dataSource: unknown): Record | undefined => { + if (!isRecord(dataSource)) return undefined + if (typeof dataSource.kind === "string" && dataSource.kind !== "route") return undefined + return isRecord(dataSource.params) ? dataSource.params : undefined +} diff --git a/packages/widgets/src/dashboard/index.ts b/packages/widgets/src/dashboard/index.ts index 4b9bcb7ff..c3f5c7f5b 100644 --- a/packages/widgets/src/dashboard/index.ts +++ b/packages/widgets/src/dashboard/index.ts @@ -19,6 +19,14 @@ export { detectSchemaVersion, migrateToLatest, } from "./migrations" +export { + dataSourceEndpoint, + dataSourceQuerySet, + dataSourceRawSql, + dataSourceRouteParams, + isQueryDataSource, + type RawSqlDataSource, +} from "./access" export { type DashboardParseOutcome, parseStoredDashboard, stampCurrentVersion } from "./parse" export { CURRENT_DASHBOARD_SCHEMA_VERSION, DashboardSchemaVersion } from "./version" export { makeWidgetDisplayConfigSchema } from "./shared/display" diff --git a/packages/widgets/src/dashboard/migrations/index.ts b/packages/widgets/src/dashboard/migrations/index.ts index 4a414ddb7..b62883bf7 100644 --- a/packages/widgets/src/dashboard/migrations/index.ts +++ b/packages/widgets/src/dashboard/migrations/index.ts @@ -32,6 +32,22 @@ export const detectSchemaVersion = (document: unknown): DashboardSchemaVersion = export const migrateToLatest = (document: unknown): Record => { if (!isPlainObject(document)) return { schemaVersion: CURRENT_DASHBOARD_SCHEMA_VERSION } + // A document declaring a version this build has never heard of comes from a + // NEWER build — a rollback, or a stale worker reading a freshly-written + // document. Return it untouched. + // + // Restamping it downward (which is what the unconditional stamp below used to + // do) is the worst available outcome: `detectSchemaVersion` reads an unknown + // version as 1, so the document would be run through the whole migration + // chain as though it were the oldest shape, then written back claiming to be + // current. Decode fails either way — but stamped, the next writer persists the + // lie and the original version is gone. Failing to decode a document we + // genuinely cannot read is recoverable; corrupting it is not. + const declared = isPlainObject(document) ? document.schemaVersion : undefined + if (typeof declared === "number" && declared > CURRENT_DASHBOARD_SCHEMA_VERSION) { + return document + } + let current: Record = document let version = detectSchemaVersion(document) diff --git a/packages/widgets/src/dashboard/migrations/migrations.test.ts b/packages/widgets/src/dashboard/migrations/migrations.test.ts index c16c39e0c..cf79c8178 100644 --- a/packages/widgets/src/dashboard/migrations/migrations.test.ts +++ b/packages/widgets/src/dashboard/migrations/migrations.test.ts @@ -222,6 +222,27 @@ describe("detectSchemaVersion", () => { }) }) +describe("migrateToLatest with a document from a newer build", () => { + // The rollback case. `detectSchemaVersion` reads an unknown version as 1, so + // without a guard the document is run through the entire chain as though it + // were the oldest shape and then stamped as current — decode fails either + // way, but stamped, the next writer persists the lie and the original + // version is gone. Failing to read is recoverable; corrupting is not. + const fromTheFuture = { ...legacyDocument, schemaVersion: 99, widgets: [] } + + it("returns it untouched rather than restamping it downward", () => { + expect(migrateToLatest(fromTheFuture)).toEqual(fromTheFuture) + }) + + it("does not claim the current version on the way out", () => { + expect(migrateToLatest(fromTheFuture).schemaVersion).toBe(99) + }) + + it("still migrates a document at or below the current version", () => { + expect(migrateToLatest(legacyDocument).schemaVersion).toBe(CURRENT_DASHBOARD_SCHEMA_VERSION) + }) +}) + describe("parseStoredDashboard", () => { it("decodes a legacy unstamped document and reports the version it came from", () => { const outcome = parse(legacyDocument) From 6dedb6cbb1d22a2ffbb8730a394af486241e7d86 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Thu, 13 Aug 2026 19:59:46 +0200 Subject: [PATCH 04/13] fix some stuff --- apps/api/package.json | 1 + .../application/metric-overview.ts | 7 +- .../src/dashboard-templates/database/redis.ts | 81 ++++++----- apps/api/src/dashboard-templates/helpers.ts | 74 +++++----- .../infrastructure/cloudflare.ts | 72 +++++----- .../dashboard-templates/messaging/kafka.ts | 82 ++++++----- apps/api/src/mcp/lib/inspect-widget.ts | 22 +-- apps/api/src/mcp/lib/raw-sql-widget.ts | 30 ++--- apps/api/src/mcp/tools/create-dashboard.ts | 25 +++- .../dashboards/perses-dashboard-import.ts | 23 ++-- apps/web/package.json | 1 + apps/web/src/api/warehouse/traces.ts | 16 +-- .../config/widget-query-builder-page.tsx | 16 +-- .../data-source-registry.test.ts | 127 ++++++++++++++++++ .../dashboard-builder/data-source-registry.ts | 57 ++++++++ .../list/dashboard-summary.ts | 45 ++++++- .../dashboard-builder/portable-dashboard.ts | 6 + .../widgets/widget-actions-context.tsx | 9 +- apps/web/src/hooks/use-widget-data.ts | 50 ++++--- apps/web/src/lib/alerts/widget-prefill.ts | 32 +++-- .../query-builder/widget-builder-shared.ts | 9 +- .../lib/query-builder/widget-builder-utils.ts | 40 +++--- bun.lock | 2 + packages/domain/src/http/query-engine.ts | 2 + packages/query-engine/src/datetime.ts | 6 +- packages/query-model/src/query-draft.ts | 16 +++ packages/query-model/src/result-shape.ts | 4 - packages/query-model/src/series-reducer.ts | 5 - packages/widgets/src/dashboard/access.test.ts | 15 +++ packages/widgets/src/dashboard/access.ts | 44 ++++-- .../widgets/src/dashboard/construct.test.ts | 93 +++++++++++++ packages/widgets/src/dashboard/construct.ts | 74 ++++++++++ packages/widgets/src/dashboard/index.ts | 9 ++ .../widgets/src/dashboard/migrations/index.ts | 2 +- 34 files changed, 767 insertions(+), 330 deletions(-) create mode 100644 apps/web/src/components/dashboard-builder/data-source-registry.test.ts create mode 100644 packages/widgets/src/dashboard/construct.test.ts create mode 100644 packages/widgets/src/dashboard/construct.ts diff --git a/apps/api/package.json b/apps/api/package.json index 6afa880c8..574761c04 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -44,6 +44,7 @@ "@maple/llm": "workspace:*", "@maple/query-engine": "workspace:*", "@maple/query-engine-integrations": "workspace:*", + "@maple/query-model": "workspace:*", "@maple/widgets": "workspace:*", "@tinybirdco/sdk": "catalog:tinybird", "drizzle-orm": "^0.45.1", diff --git a/apps/api/src/dashboard-templates/application/metric-overview.ts b/apps/api/src/dashboard-templates/application/metric-overview.ts index 8ea3f7184..877dfe0c1 100644 --- a/apps/api/src/dashboard-templates/application/metric-overview.ts +++ b/apps/api/src/dashboard-templates/application/metric-overview.ts @@ -10,10 +10,11 @@ import { templateId, } from "@/dashboard-templates/helpers" import type { TemplateDefinition, WidgetDef } from "@/dashboard-templates/types" +import { type QueryBuilderMetricType, toQueryBuilderMetricType } from "@maple/query-model" function widgets(opts: { metricName: string - metricType: string + metricType: QueryBuilderMetricType serviceName?: string aggregation?: string }): WidgetDef[] { @@ -154,7 +155,9 @@ export const metricOverviewTemplate: TemplateDefinition = { ], build: (params) => { const metricName = paramValue(params, "metric_name") ?? "" - const metricType = paramValue(params, "metric_type") ?? "sum" + // A template parameter is typed by a human into a text field, so an + // unrecognised metric type falls back rather than failing the build. + const metricType = toQueryBuilderMetricType(paramValue(params, "metric_type")) ?? "sum" const serviceName = paramValue(params, "service_name") const scope = serviceName ? ` for ${serviceName}` : "" return buildPortableDashboard({ diff --git a/apps/api/src/dashboard-templates/database/redis.ts b/apps/api/src/dashboard-templates/database/redis.ts index 9c3986ab8..6766ec2fc 100644 --- a/apps/api/src/dashboard-templates/database/redis.ts +++ b/apps/api/src/dashboard-templates/database/redis.ts @@ -79,49 +79,44 @@ function widgets(serviceName?: string): WidgetDef[] { // The number people actually act on. `unit: "percent"` is a 0–1 ratio, so no `* 100`. id: "keyspace-hit-rate", visualization: "chart", - dataSource: { - endpoint: "custom_query_builder_timeseries", - params: { - queries: [ - { - ...makeQueryDraft({ - id: "redis-hit-rate-hits", - name: "A", - dataSource: "metrics", - aggregation: "rate", - isMonotonic: true, - whereClause: where, - metricName: "redis.keyspace.hits", - metricType: "sum", - }), - hidden: true, - }, - { - ...makeQueryDraft({ - id: "redis-hit-rate-misses", - name: "B", - dataSource: "metrics", - aggregation: "rate", - isMonotonic: true, - whereClause: where, - metricName: "redis.keyspace.misses", - metricType: "sum", - }), - hidden: true, - }, - ], - formulas: [ - { - id: "redis-hit-rate", - name: "Keyspace hit rate", - expression: "A / (A + B)", - legend: "hit rate", - }, - ], - comparison: { mode: "none", includePercentChange: true }, - debug: false, - }, - }, + dataSource: makeQueryBuilderTimeseriesDataSource( + [ + { + ...makeQueryDraft({ + id: "redis-hit-rate-hits", + name: "A", + dataSource: "metrics", + aggregation: "rate", + isMonotonic: true, + whereClause: where, + metricName: "redis.keyspace.hits", + metricType: "sum", + }), + hidden: true, + }, + { + ...makeQueryDraft({ + id: "redis-hit-rate-misses", + name: "B", + dataSource: "metrics", + aggregation: "rate", + isMonotonic: true, + whereClause: where, + metricName: "redis.keyspace.misses", + metricType: "sum", + }), + hidden: true, + }, + ], + [ + { + id: "redis-hit-rate", + name: "Keyspace hit rate", + expression: "A / (A + B)", + legend: "hit rate", + }, + ], + ), display: { title: "Keyspace Hit Rate", ...CHART_DISPLAY_LINE, unit: "percent" }, layout: { x: 6, y: 6, w: 6, h: 6 }, }, diff --git a/apps/api/src/dashboard-templates/helpers.ts b/apps/api/src/dashboard-templates/helpers.ts index b3f70efd2..56e603bbc 100644 --- a/apps/api/src/dashboard-templates/helpers.ts +++ b/apps/api/src/dashboard-templates/helpers.ts @@ -4,6 +4,13 @@ import { DashboardTemplateParameterKey, PortableDashboardDocument, } from "@maple/domain/http" +import type { + QueryBuilderDataSource, + QueryBuilderFormulaPayload, + QueryBuilderMetricType, + QueryBuilderQueryDraftPayload, +} from "@maple/query-model" +import { makeQueryDataSource } from "@maple/widgets/dashboard" import type { TemplateParameterValues, WidgetDef } from "./types" /** The display half of a widget, so the shared chart presets are literal-typed. */ @@ -25,19 +32,18 @@ const decodePortableDashboard = Schema.decodeUnknownSync(PortableDashboardDocume export function makeQueryDraft(opts: { id: string name: string - dataSource: "traces" | "logs" | "metrics" + dataSource: QueryBuilderDataSource aggregation: string whereClause?: string groupBy?: string[] metricName?: string - metricType?: string + metricType?: QueryBuilderMetricType isMonotonic?: boolean -}): Record { - const draft: Record = { +}): QueryBuilderQueryDraftPayload { + const base = { id: opts.id, name: opts.name, enabled: true, - dataSource: opts.dataSource, whereClause: opts.whereClause ?? "", aggregation: opts.aggregation, stepInterval: "", @@ -54,40 +60,36 @@ export function makeQueryDraft(opts: { orderBy: "", limit: "", legend: "", - } + } as const satisfies Omit + // Metric-only fields belong solely to the metrics source. if (opts.dataSource === "metrics") { - draft.signalSource = "default" - draft.metricName = opts.metricName ?? "" - draft.metricType = opts.metricType ?? "gauge" - draft.isMonotonic = opts.isMonotonic ?? false + return { + ...base, + dataSource: "metrics", + signalSource: "default", + metricName: opts.metricName ?? "", + metricType: opts.metricType ?? "gauge", + isMonotonic: opts.isMonotonic ?? false, + } } - return draft + return { ...base, dataSource: opts.dataSource } } -export function makeQueryBuilderTimeseriesDataSource(queries: Record[]): { - endpoint: string - params: Record -} { - return { - endpoint: "custom_query_builder_timeseries", - params: { - queries, - formulas: [], - comparison: { mode: "none", includePercentChange: true }, - debug: false, - }, - } +export function makeQueryBuilderTimeseriesDataSource( + queries: QueryBuilderQueryDraftPayload[], + formulas: QueryBuilderFormulaPayload[] = [], +) { + return makeQueryDataSource({ + resultShape: "timeseries", + queries, + formulas, + comparison: { mode: "none", includePercentChange: true }, + }) } -export function makeQueryBuilderBreakdownDataSource(queries: Record[]): { - endpoint: string - params: Record -} { - return { - endpoint: "custom_query_builder_breakdown", - params: { queries }, - } +export function makeQueryBuilderBreakdownDataSource(queries: QueryBuilderQueryDraftPayload[]) { + return makeQueryDataSource({ resultShape: "breakdown", queries }) } // `seriesStats` (the Min/Max/Mean/Last table) is opt-in and costs up to 45% of a @@ -161,12 +163,12 @@ export function metricsTimeseries(opts: { id: string name: string metricName: string - metricType: string + metricType: QueryBuilderMetricType aggregation?: string whereClause?: string groupBy?: string[] isMonotonic?: boolean -}): { endpoint: string; params: Record } { +}) { return makeQueryBuilderTimeseriesDataSource([ makeQueryDraft({ id: opts.id, @@ -186,11 +188,11 @@ export function metricsBreakdown(opts: { id: string name: string metricName: string - metricType: string + metricType: QueryBuilderMetricType aggregation?: string whereClause?: string groupBy: string[] -}): { endpoint: string; params: Record } { +}) { return makeQueryBuilderBreakdownDataSource([ makeQueryDraft({ id: opts.id, diff --git a/apps/api/src/dashboard-templates/infrastructure/cloudflare.ts b/apps/api/src/dashboard-templates/infrastructure/cloudflare.ts index 2c8e0eb2c..7848e60e8 100644 --- a/apps/api/src/dashboard-templates/infrastructure/cloudflare.ts +++ b/apps/api/src/dashboard-templates/infrastructure/cloudflare.ts @@ -4,6 +4,7 @@ import { CHART_DISPLAY_LINE, buildPortableDashboard, combineWhere, + makeQueryBuilderTimeseriesDataSource, makeQueryDraft, metricsTimeseries, paramKey, @@ -20,8 +21,6 @@ function zoneWhere(zoneName?: string): string { return zoneName ? `service.name = "cloudflare/${zoneName}"` : "" } -type DataSource = { endpoint: string; params: Record } - /** * A ratio over `cloudflare.http.requests`, as two hidden query-builder queries plus a formula. * Powers both the KPI stat and the over-time chart for cache hit rate and 5xx error rate. @@ -35,48 +34,43 @@ function requestsRatioDataSource(opts: { numeratorWhere: string formulaName: string legend: string -}): DataSource { +}) { const base = { dataSource: "metrics" as const, aggregation: "sum", metricName: "cloudflare.http.requests", - metricType: "sum", - } - return { - endpoint: "custom_query_builder_timeseries", - params: { - queries: [ - { - ...makeQueryDraft({ - ...base, - id: `${opts.idPrefix}-num`, - name: "A", - whereClause: combineWhere(opts.where, opts.numeratorWhere), - }), - hidden: true, - }, - { - ...makeQueryDraft({ - ...base, - id: `${opts.idPrefix}-den`, - name: "B", - whereClause: opts.where, - }), - hidden: true, - }, - ], - formulas: [ - { - id: `${opts.idPrefix}-ratio`, - name: opts.formulaName, - expression: "A / B", - legend: opts.legend, - }, - ], - comparison: { mode: "none", includePercentChange: true }, - debug: false, - }, + metricType: "sum" as const, } + return makeQueryBuilderTimeseriesDataSource( + [ + { + ...makeQueryDraft({ + ...base, + id: `${opts.idPrefix}-num`, + name: "A", + whereClause: combineWhere(opts.where, opts.numeratorWhere), + }), + hidden: true, + }, + { + ...makeQueryDraft({ + ...base, + id: `${opts.idPrefix}-den`, + name: "B", + whereClause: opts.where, + }), + hidden: true, + }, + ], + [ + { + id: `${opts.idPrefix}-ratio`, + name: opts.formulaName, + expression: "A / B", + legend: opts.legend, + }, + ], + ) } /** diff --git a/apps/api/src/dashboard-templates/messaging/kafka.ts b/apps/api/src/dashboard-templates/messaging/kafka.ts index 6a65869ec..818c7203f 100644 --- a/apps/api/src/dashboard-templates/messaging/kafka.ts +++ b/apps/api/src/dashboard-templates/messaging/kafka.ts @@ -2,6 +2,7 @@ import { CHART_DISPLAY_BAR, CHART_DISPLAY_LINE, buildPortableDashboard, + makeQueryBuilderTimeseriesDataSource, makeQueryDraft, metricsTimeseries, paramKey, @@ -77,49 +78,44 @@ function widgets(serviceName?: string): WidgetDef[] { // bars make the excursions read as discrete incidents rather than a filled ribbon. id: "partition-under-replicated", visualization: "chart", - dataSource: { - endpoint: "custom_query_builder_timeseries", - params: { - queries: [ - { - ...makeQueryDraft({ - id: "kafka-replicas", - name: "A", - dataSource: "metrics", - aggregation: "sum", - isMonotonic: false, - whereClause: where, - metricName: "kafka.partition.replicas", - metricType: "sum", - }), - hidden: true, - }, - { - ...makeQueryDraft({ - id: "kafka-replicas-isr", - name: "B", - dataSource: "metrics", - aggregation: "sum", - isMonotonic: false, - whereClause: where, - metricName: "kafka.partition.replicas_in_sync", - metricType: "sum", - }), - hidden: true, - }, - ], - formulas: [ - { - id: "kafka-under-replicated", - name: "Under-replicated", - expression: "A - B", - legend: "replicas out of sync", - }, - ], - comparison: { mode: "none", includePercentChange: true }, - debug: false, - }, - }, + dataSource: makeQueryBuilderTimeseriesDataSource( + [ + { + ...makeQueryDraft({ + id: "kafka-replicas", + name: "A", + dataSource: "metrics", + aggregation: "sum", + isMonotonic: false, + whereClause: where, + metricName: "kafka.partition.replicas", + metricType: "sum", + }), + hidden: true, + }, + { + ...makeQueryDraft({ + id: "kafka-replicas-isr", + name: "B", + dataSource: "metrics", + aggregation: "sum", + isMonotonic: false, + whereClause: where, + metricName: "kafka.partition.replicas_in_sync", + metricType: "sum", + }), + hidden: true, + }, + ], + [ + { + id: "kafka-under-replicated", + name: "Under-replicated", + expression: "A - B", + legend: "replicas out of sync", + }, + ], + ), display: { title: "Replicas Out of Sync", ...CHART_DISPLAY_BAR, unit: "number" }, layout: { x: 6, y: 6, w: 6, h: 6 }, }, diff --git a/apps/api/src/mcp/lib/inspect-widget.ts b/apps/api/src/mcp/lib/inspect-widget.ts index ac39085f5..3771866ff 100644 --- a/apps/api/src/mcp/lib/inspect-widget.ts +++ b/apps/api/src/mcp/lib/inspect-widget.ts @@ -13,7 +13,7 @@ import { type FormulaDraft, type QueryRunResult, } from "@maple/query-engine/formula-results" -import { QueryBuilderQueryDraftSchema } from "@maple/domain/http" +import { QueryBuilderFormulaSchema, QueryBuilderQueryDraftSchema } from "@maple/domain/http" import { computeBreakdownStats, computeFlags, @@ -70,7 +70,11 @@ export type DashboardWidget = typeof DashboardWidgetSchema.Type const QueryBuilderParamsSchema = Schema.Struct({ queries: Schema.mutable(Schema.Array(QueryBuilderQueryDraftSchema)), - formulas: Schema.optional(Schema.mutable(Schema.Array(Schema.Unknown))), + // Typed rather than `Schema.Unknown`: the web timeseries server function — the + // path that actually renders these charts — already decodes formulas against + // the same required shape, so an inspection that tolerated a partial formula + // would report on a chart the renderer refuses to draw. + formulas: Schema.optional(Schema.mutable(Schema.Array(QueryBuilderFormulaSchema))), }) const decodeQueryBuilderParams = Schema.decodeUnknownEffect(QueryBuilderParamsSchema) @@ -629,15 +633,11 @@ export const inspectWidget = Effect.fn("inspectWidget")( // base timeseries by alias, so only the timeseries endpoint supports them. const formulaEvaluated = hasFormulaWarning && isTimeseries if (formulaEvaluated) { - const formulaDrafts: FormulaDraft[] = formulas.map((f, i) => { - const obj = (f ?? {}) as Record - return { - id: typeof obj.id === "string" ? obj.id : `formula-${i}`, - name: typeof obj.name === "string" ? obj.name : `Formula ${i + 1}`, - expression: typeof obj.expression === "string" ? obj.expression : "", - legend: typeof obj.legend === "string" ? obj.legend : "", - } - }) + // No per-field narrowing: `QueryBuilderFormulaSchema` already guarantees + // all four strings, and `FormulaDraft` asks for exactly them. + const formulaDrafts: FormulaDraft[] = formulas.map( + ({ id, name, expression, legend }): FormulaDraft => ({ id, name, expression, legend }), + ) for (const fr of buildFormulaResults(formulaDrafts, formulaBaseInputs)) { if (fr.status === "error") { diff --git a/apps/api/src/mcp/lib/raw-sql-widget.ts b/apps/api/src/mcp/lib/raw-sql-widget.ts index d61b34121..1bc420e98 100644 --- a/apps/api/src/mcp/lib/raw-sql-widget.ts +++ b/apps/api/src/mcp/lib/raw-sql-widget.ts @@ -1,5 +1,6 @@ import { rawSqlDisplayTypeFor, widgetTypeByVisualization } from "@maple/domain/http" import type { RawSqlDisplayType, WidgetDataSourceSchema } from "@maple/domain/http" +import { makeRawSqlDataSource } from "@maple/widgets/dashboard" // MCP-side mirror of the web's raw-SQL widget builder so agents can create // raw-SQL widgets without hand-crafting the dataSource JSON. @@ -19,29 +20,16 @@ export function buildRawSqlDataSource(args: { displayType: RawSqlDisplayType granularitySeconds?: number }): WidgetDataSource { - const params: Record = { + return makeRawSqlDataSource({ sql: args.sql, displayType: args.displayType, - } - if (args.granularitySeconds != null) { - params.granularitySeconds = args.granularitySeconds - } - - const base: WidgetDataSource = { - endpoint: "raw_sql_chart", - params, - } - - // A scalar widget needs a reduceToValue transform so the tile reads - // `data[0].value`. Mirrors buildRawSqlDataSource in the web app. - if (widgetTypeByVisualization(args.visualization)?.isScalar === true) { - return { - ...base, - transform: { reduceToValue: { field: "value", aggregate: "first" } }, - } - } - - return base + ...(args.granularitySeconds == null ? {} : { granularitySeconds: args.granularitySeconds }), + // A scalar widget needs a reduceToValue transform so the tile reads + // `data[0].value`. Mirrors buildRawSqlDataSource in the web app. + ...(widgetTypeByVisualization(args.visualization)?.isScalar === true + ? { transform: { reduceToValue: { field: "value", aggregate: "first" } } } + : {}), + }) } export function validateRawSqlMacro(sql: string): string | null { diff --git a/apps/api/src/mcp/tools/create-dashboard.ts b/apps/api/src/mcp/tools/create-dashboard.ts index 594d182b5..ee0cf1b81 100644 --- a/apps/api/src/mcp/tools/create-dashboard.ts +++ b/apps/api/src/mcp/tools/create-dashboard.ts @@ -10,6 +10,12 @@ import { findNextPosition, } from "@maple/domain/http" import { DASHBOARD_TEMPLATES, getTemplate } from "@/dashboard-templates" +import { + QUERY_BUILDER_DATA_SOURCES, + QUERY_BUILDER_METRIC_TYPES, + toQueryBuilderDataSource, + toQueryBuilderMetricType, +} from "@maple/query-model" import { collectBlockingBuilderWarnings, formatValidationSummary, @@ -93,14 +99,23 @@ function simpleSpecToWidget( return `Widget "${spec.title}": visualization must be one of ${SIMPLE_SPEC_VISUALIZATIONS.join(", ")} for simplified specs. Use add_dashboard_widget for other kinds.` } - if (!["traces", "logs", "metrics"].includes(source)) { - return `Widget "${spec.title}": source must be traces, logs, or metrics.` + const dataSource = toQueryBuilderDataSource(source) + if (dataSource === null) { + return `Widget "${spec.title}": source must be ${QUERY_BUILDER_DATA_SOURCES.join(", ")}.` } - if (source === "metrics" && (!spec.metric_name || !spec.metric_type)) { + if (dataSource === "metrics" && (!spec.metric_name || !spec.metric_type)) { return `Widget "${spec.title}": source=metrics requires metric_name and metric_type. Use list_metrics to discover.` } + // Narrowed rather than passed through: an unrecognised `metric_type` used to + // be silently coerced to `gauge`, which draws a chart that looks fine and + // aggregates a counter wrong. + const metricType = spec.metric_type === undefined ? undefined : toQueryBuilderMetricType(spec.metric_type) + if (spec.metric_type !== undefined && metricType === null) { + return `Widget "${spec.title}": metric_type must be one of ${QUERY_BUILDER_METRIC_TYPES.join(", ")}.` + } + const metric = spec.metric ?? (source === "metrics" ? "avg" : "count") const where = spec.service_name ? `service.name = "${spec.service_name}"` : "" @@ -116,12 +131,12 @@ function simpleSpecToWidget( const queryDraft = makeQueryDraft({ id: `q-${id}`, name: spec.title, - dataSource: source as "traces" | "logs" | "metrics", + dataSource, aggregation: metric, whereClause: where, groupBy, metricName: spec.metric_name, - metricType: spec.metric_type, + ...(metricType === null || metricType === undefined ? {} : { metricType }), }) const display: Record = { title: spec.title } diff --git a/apps/api/src/services/dashboards/perses-dashboard-import.ts b/apps/api/src/services/dashboards/perses-dashboard-import.ts index c4b732894..fb30be80d 100644 --- a/apps/api/src/services/dashboards/perses-dashboard-import.ts +++ b/apps/api/src/services/dashboards/perses-dashboard-import.ts @@ -10,6 +10,7 @@ import { widgetTypeByVisualization, type WidgetVisualization, } from "@maple/domain/http" +import { makeRawSqlDataSource } from "@maple/widgets/dashboard" type UnknownRecord = Record type DashboardWidget = typeof DashboardWidgetSchema.Type @@ -278,25 +279,17 @@ function rawSqlDataSource(args: { sql: string displayType: RawSqlDisplayType }): DashboardWidget["dataSource"] { - const base: DashboardWidget["dataSource"] = { - endpoint: "raw_sql_chart", - params: { - sql: args.sql, - displayType: args.displayType, - }, - } - // `displayType === "stat"` is kept alongside the panel's own scalar flag: a // Perses panel can import as a non-scalar type while its query still yields // the single-row shape a stat renders. - if (args.displayType === "stat" || widgetTypeByVisualization(args.visualization)?.isScalar === true) { - return { - ...base, - transform: { reduceToValue: { field: "value", aggregate: "first" } }, - } - } + const isScalar = + args.displayType === "stat" || widgetTypeByVisualization(args.visualization)?.isScalar === true - return base + return makeRawSqlDataSource({ + sql: args.sql, + displayType: args.displayType, + ...(isScalar ? { transform: { reduceToValue: { field: "value", aggregate: "first" } } } : {}), + }) } function markdownDataSource(): DashboardWidget["dataSource"] { diff --git a/apps/web/package.json b/apps/web/package.json index b7ebfcfbe..dcb0385dc 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -35,6 +35,7 @@ "@maple/thinking-orbs": "workspace:*", "@maple/ui": "workspace:*", "@maple/unitflow": "workspace:*", + "@maple/widgets": "workspace:*", "@rrweb/replay": "^2.0.1", "@rrweb/types": "^2.0.1", "@streamdown/cjk": "^1.0.3", diff --git a/apps/web/src/api/warehouse/traces.ts b/apps/web/src/api/warehouse/traces.ts index 48b7f1d50..a87b55bb1 100644 --- a/apps/web/src/api/warehouse/traces.ts +++ b/apps/web/src/api/warehouse/traces.ts @@ -65,10 +65,10 @@ const ListTracesInputSchema = Schema.Struct({ httpStatusCodes: Schema.optional(Schema.Array(Schema.String)), deploymentEnvs: Schema.optional(Schema.Array(DeploymentEnvironment)), namespaces: Schema.optional(Schema.Array(ServiceNamespace)), - // Singular aliases, folded into the arrays by `oneOrMany`. Saved dashboard - // widgets store their `dataSource.params` verbatim, so a dashboard created - // before this change still sends `service: "api-gw"` — dropping these keys - // would silently stop it filtering. + // Singular aliases, folded into the arrays by `oneOrMany`. A curated-route + // widget's params bag reaches the server function verbatim (see + // `toWidgetRequest`), so a dashboard created before this change still sends + // `service: "api-gw"` — dropping these keys would silently stop it filtering. service: Schema.optional(ServiceName), spanName: Schema.optional(SpanName), httpMethod: Schema.optional(Schema.String), @@ -510,10 +510,10 @@ const GetTracesFacetsInputSchema = Schema.Struct({ httpStatusCodes: Schema.optional(Schema.Array(Schema.String)), deploymentEnvs: Schema.optional(Schema.Array(DeploymentEnvironment)), namespaces: Schema.optional(Schema.Array(ServiceNamespace)), - // Singular aliases, folded into the arrays by `oneOrMany`. Saved dashboard - // widgets store their `dataSource.params` verbatim, so a dashboard created - // before this change still sends `service: "api-gw"` — dropping these keys - // would silently stop it filtering. + // Singular aliases, folded into the arrays by `oneOrMany`. A curated-route + // widget's params bag reaches the server function verbatim (see + // `toWidgetRequest`), so a dashboard created before this change still sends + // `service: "api-gw"` — dropping these keys would silently stop it filtering. service: Schema.optional(ServiceName), spanName: Schema.optional(SpanName), httpMethod: Schema.optional(Schema.String), diff --git a/apps/web/src/components/dashboard-builder/config/widget-query-builder-page.tsx b/apps/web/src/components/dashboard-builder/config/widget-query-builder-page.tsx index 3e67c55f4..b4d6e6b94 100644 --- a/apps/web/src/components/dashboard-builder/config/widget-query-builder-page.tsx +++ b/apps/web/src/components/dashboard-builder/config/widget-query-builder-page.tsx @@ -1,5 +1,6 @@ import * as React from "react" import { widgetTypeByVisualization } from "@maple/domain/http" +import { dataSourceRawSql } from "@maple/widgets/dashboard" import { Button } from "@maple/ui/components/ui/button" import { Tabs, TabsList, TabsTrigger } from "@maple/ui/components/ui/tabs" @@ -76,16 +77,9 @@ const WidgetPreview = React.memo(function WidgetPreview({ widget }: { widget: Da type SourceMode = "builder" | "rawSql" function readRawSqlDraftFromWidget(widget: DashboardWidget): RawSqlDraft { - const params = (widget.dataSource.params ?? {}) as { - sql?: unknown - granularitySeconds?: unknown - } - if (widget.dataSource.endpoint === "raw_sql_chart" && typeof params.sql === "string") { - return { - sql: params.sql, - granularitySeconds: - typeof params.granularitySeconds === "number" ? params.granularitySeconds : null, - } + const rawSql = dataSourceRawSql(widget.dataSource) + if (rawSql !== null && rawSql.sql.length > 0) { + return { sql: rawSql.sql, granularitySeconds: rawSql.granularitySeconds ?? null } } const displayType = visualizationToDisplayType(widget.visualization, widget.display.chartId) return { sql: RAW_SQL_TEMPLATES[displayType], granularitySeconds: null } @@ -163,7 +157,7 @@ export function WidgetQueryBuilderPage({ actions: { setTimeRange }, } = useDashboardTimeRange() - const initialMode: SourceMode = widget.dataSource.endpoint === "raw_sql_chart" ? "rawSql" : "builder" + const initialMode: SourceMode = dataSourceRawSql(widget.dataSource) !== null ? "rawSql" : "builder" const [mode, setMode] = React.useState(initialMode) const initialModeRef = React.useRef(initialMode) diff --git a/apps/web/src/components/dashboard-builder/data-source-registry.test.ts b/apps/web/src/components/dashboard-builder/data-source-registry.test.ts new file mode 100644 index 000000000..c702b6748 --- /dev/null +++ b/apps/web/src/components/dashboard-builder/data-source-registry.test.ts @@ -0,0 +1,127 @@ +import { describe, expect, it } from "vitest" +import { getServerFunction, toWidgetRequest } from "./data-source-registry" + +/** + * `toWidgetRequest` is the whole v3 seam on the read path: every widget fetch + * goes through it, and its output feeds the atom family key, so a drift here is + * both a wrong query and a poisoned cache entry. + * + * The cases below are stated as "the stored data source" → "the request the old + * endpoint/params code sent", because at schema v2 the two MUST be identical. + * The v3 arm of each pair asserts the same request from the shape it migrates + * to, which is the property that makes the version flip a no-op here. + */ + +const draft = { id: "q1", name: "A", aggregation: "count", dataSource: "traces" } + +describe("toWidgetRequest — query-builder widgets", () => { + it("sends the same endpoint and params v2 stored", () => { + const v2 = { + endpoint: "custom_query_builder_timeseries", + params: { queries: [draft], formulas: [], comparison: { mode: "none" } }, + } + expect(toWidgetRequest(v2)).toEqual({ + endpoint: "custom_query_builder_timeseries", + params: { queries: [draft], formulas: [], comparison: { mode: "none" } }, + }) + }) + + it("builds the identical request from the v3 shape", () => { + expect( + toWidgetRequest({ + kind: "query", + resultShape: "timeseries", + queries: [draft], + formulas: [], + comparison: { mode: "none" }, + }), + ).toEqual( + toWidgetRequest({ + endpoint: "custom_query_builder_timeseries", + params: { queries: [draft], formulas: [], comparison: { mode: "none" } }, + }), + ) + }) + + it("maps each result shape onto the server function that already serves it", () => { + for (const [shape, endpoint] of [ + ["timeseries", "custom_query_builder_timeseries"], + ["breakdown", "custom_query_builder_breakdown"], + ["list", "custom_query_builder_list"], + ] as const) { + const request = toWidgetRequest({ kind: "query", resultShape: shape, queries: [] }) + expect(request?.endpoint).toBe(endpoint) + expect(getServerFunction(request?.endpoint ?? "")).toBeDefined() + } + }) + + it("omits formulas and comparison the widget never had", () => { + // Not `formulas: []` — an empty array is a value the server function would + // see as "formulas were configured and are empty". + expect( + toWidgetRequest({ endpoint: "custom_query_builder_list", params: { queries: [draft] } })?.params, + ).toEqual({ queries: [draft] }) + }) +}) + +describe("toWidgetRequest — raw SQL", () => { + it("sends the same params v2 stored", () => { + expect( + toWidgetRequest({ + endpoint: "raw_sql_chart", + params: { sql: "SELECT 1", displayType: "line", granularitySeconds: 300 }, + }), + ).toEqual({ + endpoint: "raw_sql_chart", + params: { sql: "SELECT 1", displayType: "line", granularitySeconds: 300 }, + }) + }) + + it("builds the identical request from the v3 shape", () => { + expect(toWidgetRequest({ kind: "raw_sql", sql: "SELECT 1", displayType: "line" })).toEqual( + toWidgetRequest({ endpoint: "raw_sql_chart", params: { sql: "SELECT 1", displayType: "line" } }), + ) + }) +}) + +describe("toWidgetRequest — curated routes", () => { + it("passes the endpoint and its opaque bag straight through", () => { + expect(toWidgetRequest({ endpoint: "service_overview", params: { limit: 5 } })).toEqual({ + endpoint: "service_overview", + params: { limit: 5 }, + }) + expect( + toWidgetRequest({ kind: "route", endpoint: "service_overview", params: { limit: 5 } }), + ).toEqual({ endpoint: "service_overview", params: { limit: 5 } }) + }) + + it("gives a route with no bag an empty one, not undefined", () => { + // The caller spreads `request.params` into the interpolated payload. + expect(toWidgetRequest({ endpoint: "list_traces" })).toEqual({ + endpoint: "list_traces", + params: {}, + }) + }) + + it("still routes markdown, which the hook special-cases before fetching", () => { + expect(toWidgetRequest({ endpoint: "markdown_static" })?.endpoint).toBe("markdown_static") + }) + + it("keeps the legacy pre-query-builder endpoints working", () => { + for (const endpoint of ["custom_timeseries", "custom_breakdown"]) { + expect(toWidgetRequest({ endpoint, params: { x: 1 } })).toEqual({ endpoint, params: { x: 1 } }) + expect(getServerFunction(endpoint)).toBeDefined() + } + }) +}) + +describe("toWidgetRequest — nothing to serve", () => { + it("returns null rather than a request that cannot resolve", () => { + // The hook reports this as a disabled tile. Returning a bogus endpoint + // instead would burn the fetch's two retries on a certain failure. + expect(toWidgetRequest({ kind: "static" })).toBeNull() + expect(toWidgetRequest({})).toBeNull() + expect(toWidgetRequest(null)).toBeNull() + expect(toWidgetRequest(undefined)).toBeNull() + }) +}) diff --git a/apps/web/src/components/dashboard-builder/data-source-registry.ts b/apps/web/src/components/dashboard-builder/data-source-registry.ts index baf761843..69643f22d 100644 --- a/apps/web/src/components/dashboard-builder/data-source-registry.ts +++ b/apps/web/src/components/dashboard-builder/data-source-registry.ts @@ -1,6 +1,14 @@ import { Effect } from "effect" import type { DataSourceEndpoint } from "@/components/dashboard-builder/types" import type { BackendError, WarehouseApiError } from "@/api/warehouse/effect-utils" +import { + dataSourceEndpoint, + dataSourceQuerySet, + dataSourceRawSql, + dataSourceRouteParams, + QUERY_SHAPE_ENDPOINTS, + RAW_SQL_ENDPOINT, +} from "@maple/widgets/dashboard" import { getServiceUsage } from "@/api/warehouse/service-usage" import { getServiceOverview, getServiceApdexTimeSeries, getServicesFacets } from "@/api/warehouse/services" @@ -79,3 +87,52 @@ export function getServerFunction(endpoint: string): ServerFunction | undefined ? serverFunctionMap[endpoint as DataSourceEndpoint] : undefined } + +/** + * A stored data source, resolved to the request the fetch layer sends. + * + * The single place the web app turns "what this widget is" into "which server + * function, with which params". Everything downstream of it — the atom family + * key, the retention namespace, `fetchWidgetData`'s dispatch — stays keyed by + * endpoint string, because that string is the transport identity and works + * unchanged either way. + * + * It is also the one function the v3 flip touches on the read path: a + * `kind: "query"` data source has no endpoint of its own, so its result shape is + * mapped onto the server function that already serves that shape. Returns null + * for a data source nothing can serve, which the caller reports as a disabled + * tile rather than a failed fetch. + */ +export function toWidgetRequest( + dataSource: unknown, +): { endpoint: string; params: Record } | null { + const rawSql = dataSourceRawSql(dataSource) + if (rawSql !== null) { + return { + endpoint: RAW_SQL_ENDPOINT, + params: { + sql: rawSql.sql, + ...(rawSql.displayType === undefined ? {} : { displayType: rawSql.displayType }), + ...(rawSql.granularitySeconds === undefined + ? {} + : { granularitySeconds: rawSql.granularitySeconds }), + }, + } + } + + const querySet = dataSourceQuerySet(dataSource) + if (querySet !== null) { + return { + endpoint: QUERY_SHAPE_ENDPOINTS[querySet.resultShape], + params: { + queries: querySet.queries, + ...(querySet.formulas === undefined ? {} : { formulas: querySet.formulas }), + ...(querySet.comparison === undefined ? {} : { comparison: querySet.comparison }), + }, + } + } + + const endpoint = dataSourceEndpoint(dataSource) + if (endpoint === null) return null + return { endpoint, params: dataSourceRouteParams(dataSource) ?? {} } +} diff --git a/apps/web/src/components/dashboard-builder/list/dashboard-summary.ts b/apps/web/src/components/dashboard-builder/list/dashboard-summary.ts index c6a23eadd..c7fa4f5e9 100644 --- a/apps/web/src/components/dashboard-builder/list/dashboard-summary.ts +++ b/apps/web/src/components/dashboard-builder/list/dashboard-summary.ts @@ -4,6 +4,7 @@ import type { DashboardSortOption } from "@/atoms/dashboard-preferences-atoms" import type { Dashboard } from "@/components/dashboard-builder/types" +import { dataSourceEndpoint, dataSourceQuerySet, dataSourceRawSql } from "@maple/widgets/dashboard" const plural = (count: number, one: string, many: string) => `${count} ${count === 1 ? one : many}` @@ -16,6 +17,11 @@ export const widgetCountLabel = (dashboard: Dashboard): string => * to server functions but carries no labels, so this is the one place that * decides how an endpoint reads to a human. * + * Only CURATED routes are keyed here. A query-builder widget's domain comes from + * the signal its drafts actually read ({@link DRAFT_SOURCE_DOMAIN}), and raw SQL + * from {@link dataSourceRawSql} — both structural, so they keep working once the + * stored data source has no endpoint to key on. + * * `markdown_static` is deliberately absent: a markdown widget reads from * nothing, so it is ignored here unless it is *all* a dashboard has, which * {@link readsFromLabel} reports as "static only". @@ -41,10 +47,19 @@ export const ENDPOINT_DOMAIN: Record = { metrics_summary: "Metrics", custom_timeseries: "Metrics", custom_breakdown: "Metrics", - custom_query_builder_timeseries: "Metrics", - custom_query_builder_breakdown: "Metrics", - custom_query_builder_list: "Metrics", - raw_sql_chart: "Raw SQL", +} + +/** + * A query-builder draft's signal, as a domain. + * + * This is also a correction: the three `custom_query_builder_*` endpoints used + * to be keyed to "Metrics" wholesale, so a dashboard of trace charts built in + * the query builder advertised itself as reading metrics. The draft knows. + */ +const DRAFT_SOURCE_DOMAIN: Record = { + traces: "Traces", + logs: "Logs", + metrics: "Metrics", } export type DashboardDomain = "Traces" | "Logs" | "Errors" | "Metrics" | "Raw SQL" @@ -55,11 +70,29 @@ const DOMAIN_ORDER: ReadonlyArray = ["Traces", "Logs", "Errors" /** How many domains the lane shows before folding the rest into "+N". */ const MAX_DOMAIN_TERMS = 2 +/** Every domain one widget reads. A multi-query chart can read more than one. */ +const widgetDomains = (dataSource: unknown): ReadonlyArray => { + if (dataSourceRawSql(dataSource) !== null) return ["Raw SQL"] + + const querySet = dataSourceQuerySet(dataSource) + if (querySet !== null) { + const domains: DashboardDomain[] = [] + for (const query of querySet.queries) { + const domain = DRAFT_SOURCE_DOMAIN[query?.dataSource] + if (domain) domains.push(domain) + } + return domains + } + + const endpoint = dataSourceEndpoint(dataSource) + const domain = endpoint === null ? undefined : ENDPOINT_DOMAIN[endpoint] + return domain ? [domain] : [] +} + export const dashboardDomains = (dashboard: Dashboard): ReadonlyArray => { const found = new Set() for (const widget of dashboard.widgets) { - const domain = ENDPOINT_DOMAIN[widget.dataSource.endpoint] - if (domain) found.add(domain) + for (const domain of widgetDomains(widget.dataSource)) found.add(domain) } return DOMAIN_ORDER.filter((domain) => found.has(domain)) } diff --git a/apps/web/src/components/dashboard-builder/portable-dashboard.ts b/apps/web/src/components/dashboard-builder/portable-dashboard.ts index 3d91cb1a1..a28eeff25 100644 --- a/apps/web/src/components/dashboard-builder/portable-dashboard.ts +++ b/apps/web/src/components/dashboard-builder/portable-dashboard.ts @@ -81,6 +81,12 @@ export function toPortableDashboard(dashboard: Dashboard): PortableDashboard { } } +// Deliberately NOT routed through `dataSourceRouteParams`: this is defensive +// hygiene over hand-written or externally-produced portable JSON, where a baked +// absolute window can appear under any endpoint, not just a curated route. It +// reads the stored bag directly because it is a bag-level scrub. Once v3 lands +// the query and raw-SQL arms carry no bag at all, so this narrows to routes on +// its own — via the migration, not via a guard here. function stripWidgetTimeParams(widget: DashboardWidget): DashboardWidget { const params = widget.dataSource.params if (!params || !("startTime" in params || "endTime" in params)) return widget diff --git a/apps/web/src/components/dashboard-builder/widgets/widget-actions-context.tsx b/apps/web/src/components/dashboard-builder/widgets/widget-actions-context.tsx index 2f2dbca5e..484968b6d 100644 --- a/apps/web/src/components/dashboard-builder/widgets/widget-actions-context.tsx +++ b/apps/web/src/components/dashboard-builder/widgets/widget-actions-context.tsx @@ -117,11 +117,10 @@ export function WidgetActionsProvider({ widget: { id: widget.id, visualization: widget.visualization, - dataSource: { - endpoint: widget.dataSource.endpoint, - params: widget.dataSource.params, - transform: widget.dataSource.transform, - }, + // The whole data source rather than three hand-picked fields: + // the prefill reads it through the version-agnostic accessors, + // and a field list here would have to grow with every v3 arm. + dataSource: widget.dataSource, display: { title: widget.display.title }, }, }) diff --git a/apps/web/src/hooks/use-widget-data.ts b/apps/web/src/hooks/use-widget-data.ts index d73623d92..3f658d212 100644 --- a/apps/web/src/hooks/use-widget-data.ts +++ b/apps/web/src/hooks/use-widget-data.ts @@ -6,7 +6,7 @@ import { useDashboardTimeRange } from "@/components/dashboard-builder/dashboard- import { useDashboardVariablesOptional } from "@/components/dashboard-builder/dashboard-variables-context" import { useWidgetTimeRangeOverride } from "@/components/dashboard-builder/widgets/widget-time-range-context" import { resolveTimeRange } from "@/atoms/dashboard-time-range-atoms" -import { getServerFunction } from "@/components/dashboard-builder/data-source-registry" +import { getServerFunction, toWidgetRequest } from "@/components/dashboard-builder/data-source-registry" import { hasUnresolvedVariableRefs, interpolateWidgetParams } from "@maple/query-engine" import type { DashboardWidget, TimeRange, WidgetDataSource } from "@/components/dashboard-builder/types" @@ -387,6 +387,12 @@ export function useWidgetDataSource( const override = timeRangeOverride ?? contextOverride ?? undefined const overrideKey = override ? encodeKey(override) : null + // The stored data source resolved to an endpoint + params, once. Everything + // below reads `request` rather than `dataSource` so the fetch path never + // touches the stored shape — the v2 → v3 flip lands entirely inside + // `toWidgetRequest`. Null means no server function can serve it. + const request = useMemo(() => toWidgetRequest(dataSource), [dataSource]) + const effectiveTimeRange = useMemo( () => (override ? resolveTimeRange(override) : dashboardTimeRange), // Keyed on the serialized override, not its identity: the dashboard object @@ -403,8 +409,8 @@ export function useWidgetDataSource( // here rather than left to the API so the tile never fires a request that is // certain to 400 (and never burns the fetch's two retries on it). const exceedsListCap = - dataSource !== undefined && - LIST_ENDPOINTS.has(dataSource.endpoint) && + request !== null && + LIST_ENDPOINTS.has(request.endpoint) && effectiveTimeRange !== null && rangeSecondsOf(effectiveTimeRange) > MAX_LIST_RANGE_SECONDS @@ -423,20 +429,22 @@ export function useWidgetDataSource( }, [narrowed, effectiveTimeRange]) const variablesContext = useDashboardVariablesOptional() - const isStatic = dataSource?.endpoint === "markdown_static" - const hasServerFn = dataSource ? !!getServerFunction(dataSource.endpoint) : false + const isStatic = request?.endpoint === "markdown_static" + const hasServerFn = request !== null && !!getServerFunction(request.endpoint) const disableReason = !dataSource ? "No data source configured" - : isStatic - ? null - : !resolvedTimeRange - ? override - ? "Unable to resolve this widget's time range" - : "Unable to resolve dashboard time range" - : !hasServerFn - ? `Unknown data source endpoint: ${dataSource.endpoint}` - : null + : request === null + ? "Unsupported data source" + : isStatic + ? null + : !resolvedTimeRange + ? override + ? "Unable to resolve this widget's time range" + : "Unable to resolve dashboard time range" + : !hasServerFn + ? `Unknown data source endpoint: ${request.endpoint}` + : null // A params blob referencing a defined dashboard variable whose value hasn't // resolved yet (query-variable options still loading, no default) must not @@ -446,11 +454,11 @@ export function useWidgetDataSource( () => variablesContext !== null && hasUnresolvedVariableRefs( - dataSource?.params, + request?.params, variablesContext.variables.map((variable) => variable.name), variablesContext.values, ), - [dataSource?.params, variablesContext], + [request?.params, variablesContext], ) const variableValues = variablesContext?.values @@ -459,7 +467,7 @@ export function useWidgetDataSource( if (!resolvedTimeRange) return {} const base = interpolateParams( { - ...dataSource?.params, + ...request?.params, strategy: { enableEmptyRangeFallback: false }, startTime: resolvedTimeRange.startTime, endTime: resolvedTimeRange.endTime, @@ -467,7 +475,7 @@ export function useWidgetDataSource( resolvedTimeRange, ) return variableValues ? interpolateWidgetParams(base, variableValues) : base - }, [resolvedTimeRange, dataSource?.params, variableValues]) + }, [resolvedTimeRange, request?.params, variableValues]) // Stabilise the atom reference across renders. Atom.family already dedupes // by encoded key, but giving React the same Atom instance avoids any path @@ -477,7 +485,7 @@ export function useWidgetDataSource( if ( disableReason !== null || isStatic || - !dataSource || + request === null || !enabled || waitingOnVariables || (exceedsListCap && !narrowed) @@ -485,13 +493,13 @@ export function useWidgetDataSource( return disabledResultAtom() } return widgetFetchAtom({ - endpoint: dataSource.endpoint, + endpoint: request.endpoint, params: resolvedParams, }) }, [ disableReason, isStatic, - dataSource, + request, resolvedParams, enabled, waitingOnVariables, diff --git a/apps/web/src/lib/alerts/widget-prefill.ts b/apps/web/src/lib/alerts/widget-prefill.ts index 9d252ada5..0567f3e23 100644 --- a/apps/web/src/lib/alerts/widget-prefill.ts +++ b/apps/web/src/lib/alerts/widget-prefill.ts @@ -2,6 +2,7 @@ import type { QueryBuilderQueryDraftPayload } from "@maple/domain/http" import { normalizeRuleQueryDraft, rawSqlHasValueColumn, type RuleFormState } from "@/lib/alerts/form-utils" import { buildTimeseriesQuerySpec } from "@maple/query-engine/query-builder" +import { dataSourceQuerySet, dataSourceRawSql } from "@maple/widgets/dashboard" export type WidgetAlertPrefillNotice = { severity: "warning" | "error" @@ -29,12 +30,6 @@ type DashboardWithWidgets = { widgets: readonly AlertableDashboardWidget[] } -const QUERY_BUILDER_ENDPOINTS = new Set([ - "custom_query_builder_timeseries", - "custom_query_builder_breakdown", - "custom_query_builder_list", -]) - function record(value: unknown): Record { return value != null && typeof value === "object" ? (value as Record) : {} } @@ -76,9 +71,9 @@ function hasHiddenSeries( return Array.isArray(hideSeries.baseNames) && hideSeries.baseNames.length > 0 } -function comparisonEnabled(params: Record): boolean { - const comparison = record(params.comparison) - return typeof comparison.mode === "string" && comparison.mode !== "none" +function comparisonEnabled(comparison: unknown): boolean { + const mode = record(comparison).mode + return typeof mode === "string" && mode !== "none" } function queryToForm( @@ -104,12 +99,15 @@ export function createWidgetAlertPrefill( widget: AlertableDashboardWidget, base: RuleFormState, ): WidgetAlertPrefillResult { - const endpoint = widget.dataSource?.endpoint - const params = record(widget.dataSource?.params) + // Structural, not endpoint-string: this is the "create an alert from this + // chart" path, and it has to keep reading a widget once the stored data + // source flips to the typed v3 union. + const rawSql = dataSourceRawSql(widget.dataSource) + const querySet = dataSourceQuerySet(widget.dataSource) const notices: WidgetAlertPrefillNotice[] = [] - if (endpoint === "raw_sql_chart") { - const sql = typeof params.sql === "string" ? params.sql : "" + if (rawSql !== null) { + const sql = rawSql.sql if (sql.trim().length === 0) { notices.push({ severity: "warning", @@ -143,8 +141,8 @@ export function createWidgetAlertPrefill( } } - if (endpoint != null && QUERY_BUILDER_ENDPOINTS.has(endpoint)) { - const queries = Array.isArray(params.queries) ? params.queries.filter(isQueryDraftPayload) : [] + if (querySet !== null) { + const queries = querySet.queries.filter(isQueryDraftPayload) const selectedIndex = queries.findIndex(isEnabledVisibleQuery) const selected = selectedIndex >= 0 @@ -171,7 +169,7 @@ export function createWidgetAlertPrefill( message: `This chart has ${visibleEnabledCount} visible queries; the alert uses ${queryLabel(selected, selectedIndex)} only.`, }) } - const formulas = Array.isArray(params.formulas) ? params.formulas : [] + const formulas = querySet.formulas ?? [] if (formulas.length > 0) { notices.push({ severity: "warning", @@ -179,7 +177,7 @@ export function createWidgetAlertPrefill( "Chart formulas are not represented in alert rules yet; the alert uses the selected base query.", }) } - if (comparisonEnabled(params)) { + if (comparisonEnabled(querySet.comparison)) { notices.push({ severity: "warning", message: diff --git a/apps/web/src/lib/query-builder/widget-builder-shared.ts b/apps/web/src/lib/query-builder/widget-builder-shared.ts index dc8fba258..08d1d87a0 100644 --- a/apps/web/src/lib/query-builder/widget-builder-shared.ts +++ b/apps/web/src/lib/query-builder/widget-builder-shared.ts @@ -308,7 +308,14 @@ export function deriveDefaultWidgetTitle(queries: readonly QueryBuilderQueryDraf } /** Reads a persisted widget's `params.queries` back into editor drafts. */ -export function loadQueryDrafts(params: Record): { +/** + * Editor drafts from a stored query set. + * + * Takes `{ queries, formulas }` rather than a params bag so callers hand it the + * result of `dataSourceQuerySet` — the accessor that reads v2 and v3 alike — + * instead of reaching into `dataSource.params` themselves. + */ +export function loadQueryDrafts(params: { queries?: unknown; formulas?: unknown }): { queries: QueryBuilderQueryDraft[] formulas: QueryBuilderFormulaDraft[] } { diff --git a/apps/web/src/lib/query-builder/widget-builder-utils.ts b/apps/web/src/lib/query-builder/widget-builder-utils.ts index a5f51c838..4f5fbc938 100644 --- a/apps/web/src/lib/query-builder/widget-builder-utils.ts +++ b/apps/web/src/lib/query-builder/widget-builder-utils.ts @@ -20,18 +20,7 @@ import { type QueryBuilderWidgetState, } from "@/lib/query-builder/widget-builder-shared" import { WIDGET_TYPES } from "@maple/domain/http" - -/** - * Endpoints whose `params.queries` are query-builder drafts. Histograms with no - * group-by persist their raw value rows through the list endpoint — without it - * the preset's query is dropped the moment the widget is opened and replaced by - * the legacy fallback draft. - */ -const QUERY_BUILDER_ENDPOINTS = new Set([ - "custom_query_builder_timeseries", - "custom_query_builder_breakdown", - "custom_query_builder_list", -]) +import { dataSourceQuerySet, dataSourceRouteParams } from "@maple/widgets/dashboard" // Lowering the widget editor's state to a persisted widget, and back. // @@ -54,11 +43,15 @@ const definitionForState = (state: QueryBuilderWidgetState) => // Persisted widget → editor state export function toInitialState(widget: DashboardWidget): QueryBuilderWidgetState { - const params = (widget.dataSource.params ?? {}) as Record - const rawComparison = - params.comparison && typeof params.comparison === "object" - ? (params.comparison as Record) - : {} + // `querySet` is non-null for every widget whose queries are query-builder + // drafts — including the list shape, which is how a histogram with no group-by + // persists its raw value rows. Reading it structurally rather than testing an + // endpoint name is what keeps this working when the stored shape flips to v3. + const querySet = dataSourceQuerySet(widget.dataSource) + // Only a legacy pre-query-builder widget (`custom_timeseries` and friends) + // still needs its raw bag, and only for the fallback draft below. + const routeParams = dataSourceRouteParams(widget.dataSource) ?? {} + const rawComparison = querySet?.comparison ?? {} // Normalize the (visualization, chartId) pair through the panel-type map on // open. This repairs widgets corrupted by the old "Chart Style" dropdown — @@ -100,10 +93,13 @@ export function toInitialState(widget: DashboardWidget): QueryBuilderWidgetState typeof rawComparison.includePercentChange === "boolean" ? rawComparison.includePercentChange : true, - debug: params.debug === true, + debug: routeParams.debug === true, statAggregate: "first", statValueField: "", - unit: widget.display.unit ?? inferDefaultUnitForQueries(loadQueryDrafts(params).queries) ?? "number", + unit: + widget.display.unit ?? + inferDefaultUnitForQueries(loadQueryDrafts(querySet ?? {}).queries) ?? + "number", legendPosition, // The Min/Max/Mean/Last table is opt-in: it costs up to 45% of the widget's // height, so a widget only shows it by asking for it. Widgets persisted @@ -135,12 +131,12 @@ export function toInitialState(widget: DashboardWidget): QueryBuilderWidgetState return { ...shared, queries: [createQueryDraft(0)], formulas: [] } } - if (QUERY_BUILDER_ENDPOINTS.has(widget.dataSource.endpoint)) { - const { queries, formulas } = loadQueryDrafts(params) + if (querySet !== null) { + const { queries, formulas } = loadQueryDrafts(querySet) if (queries.length > 0) return { ...shared, queries, formulas } } - return { ...shared, queries: [legacyQueryDraft(params)], formulas: [] } + return { ...shared, queries: [legacyQueryDraft(routeParams)], formulas: [] } } // Editor state → persisted widget diff --git a/bun.lock b/bun.lock index 195fdecb5..ca79f21d9 100644 --- a/bun.lock +++ b/bun.lock @@ -62,6 +62,7 @@ "@maple/llm": "workspace:*", "@maple/query-engine": "workspace:*", "@maple/query-engine-integrations": "workspace:*", + "@maple/query-model": "workspace:*", "@maple/widgets": "workspace:*", "@tinybirdco/sdk": "catalog:tinybird", "drizzle-orm": "^0.45.1", @@ -271,6 +272,7 @@ "@maple/thinking-orbs": "workspace:*", "@maple/ui": "workspace:*", "@maple/unitflow": "workspace:*", + "@maple/widgets": "workspace:*", "@rrweb/replay": "^2.0.1", "@rrweb/types": "^2.0.1", "@streamdown/cjk": "^1.0.3", diff --git a/packages/domain/src/http/query-engine.ts b/packages/domain/src/http/query-engine.ts index 123e58c4d..107fc1d8f 100644 --- a/packages/domain/src/http/query-engine.ts +++ b/packages/domain/src/http/query-engine.ts @@ -1570,6 +1570,8 @@ export { LogsQueryDraftSchema, MetricsQueryDraftSchema, QueryBuilderAddOnsSchema, + QueryBuilderFormulaSchema, + type QueryBuilderFormulaPayload, type QueryBuilderQueryDraftPayload, QueryBuilderQueryDraftSchema, TracesQueryDraftSchema, diff --git a/packages/query-engine/src/datetime.ts b/packages/query-engine/src/datetime.ts index c40590f96..acd36c179 100644 --- a/packages/query-engine/src/datetime.ts +++ b/packages/query-engine/src/datetime.ts @@ -333,12 +333,10 @@ export function computeBucketSeconds( const maxBucketForMin = Math.floor(rangeSeconds / minBuckets) if (bucket > maxBucketForMin) { const finer = rungs.filter((candidate) => candidate <= maxBucketForMin) + // `rungs[0]` rather than the raw ladder's floor: `minBuckets` must not be + // allowed to step below the caller's `minBucketSeconds`. bucket = finer.length > 0 ? finer[finer.length - 1] : rungs[0] } - // `minBuckets` may have stepped below the caller's floor on a short window. - if (bucket < minBucketSeconds) { - bucket = rungs[0] - } return bucket } diff --git a/packages/query-model/src/query-draft.ts b/packages/query-model/src/query-draft.ts index 87cc2c2ca..415f4e20c 100644 --- a/packages/query-model/src/query-draft.ts +++ b/packages/query-model/src/query-draft.ts @@ -20,6 +20,22 @@ export type QueryBuilderMetricType = (typeof QUERY_BUILDER_METRIC_TYPES)[number] export const QUERY_BUILDER_SIGNAL_SOURCES = ["default", "meter"] as const export type QueryBuilderSignalSource = (typeof QUERY_BUILDER_SIGNAL_SOURCES)[number] +/** + * Narrow a free-form string onto the closed sets above. + * + * The inputs that need this are agent-authored (MCP tool params) and + * user-authored (dashboard template parameters), where a wrong value is a + * plausible mistake rather than a bug. Returning `null` rather than a default + * keeps the choice at the call site: the MCP tools reject with a message naming + * the valid values, while the template path falls back — an agent that asked for + * `metric_type: "counter"` should be told, not quietly given a gauge. + */ +export const toQueryBuilderDataSource = (value: unknown): QueryBuilderDataSource | null => + QUERY_BUILDER_DATA_SOURCES.find((candidate) => candidate === value) ?? null + +export const toQueryBuilderMetricType = (value: unknown): QueryBuilderMetricType | null => + QUERY_BUILDER_METRIC_TYPES.find((candidate) => candidate === value) ?? null + export const QueryBuilderAddOnsSchema = Schema.Struct({ groupBy: Schema.Boolean, having: Schema.Boolean, diff --git a/packages/query-model/src/result-shape.ts b/packages/query-model/src/result-shape.ts index 9f04a4945..7f2f936c1 100644 --- a/packages/query-model/src/result-shape.ts +++ b/packages/query-model/src/result-shape.ts @@ -1,5 +1,3 @@ -import { Schema } from "effect" - /** * What a query set is asked to return. * @@ -10,5 +8,3 @@ import { Schema } from "effect" */ export const QUERY_RESULT_SHAPES = ["timeseries", "breakdown", "list"] as const export type QueryResultShape = (typeof QUERY_RESULT_SHAPES)[number] - -export const QueryResultShapeSchema = Schema.Literals(QUERY_RESULT_SHAPES) diff --git a/packages/query-model/src/series-reducer.ts b/packages/query-model/src/series-reducer.ts index d1ed881bb..d7ce8cca3 100644 --- a/packages/query-model/src/series-reducer.ts +++ b/packages/query-model/src/series-reducer.ts @@ -42,8 +42,3 @@ export const ALERT_REDUCERS = REDUCER_TABLE.flatMap((entry) => export const ALERT_REDUCER_TO_SERIES_REDUCER = Object.fromEntries( REDUCER_TABLE.flatMap((entry) => (entry.alert === null ? [] : [[entry.alert, entry.series] as const])), ) as Record - -/** Partial in the other direction: `"count"` has no alert spelling. */ -export const SERIES_REDUCER_TO_ALERT_REDUCER = Object.fromEntries( - REDUCER_TABLE.flatMap((entry) => (entry.alert === null ? [] : [[entry.series, entry.alert] as const])), -) as Partial> diff --git a/packages/widgets/src/dashboard/access.test.ts b/packages/widgets/src/dashboard/access.test.ts index 021e4b11c..9e7d794d7 100644 --- a/packages/widgets/src/dashboard/access.test.ts +++ b/packages/widgets/src/dashboard/access.test.ts @@ -139,6 +139,21 @@ describe("dataSourceRouteParams", () => { expect(dataSourceRouteParams({ kind: "query", resultShape: "list", queries: [] })).toBeUndefined() expect(dataSourceRouteParams({ kind: "raw_sql", sql: "SELECT 1" })).toBeUndefined() }) + + it("refuses the v2 bag of a query-builder or raw-SQL widget", () => { + // The asymmetry guard: on v2 these physically have a `params` bag, and + // handing it back would give v2 an answer v3 cannot give — a caller written + // against it would break at the version flip, silently. + expect( + dataSourceRouteParams({ + endpoint: "custom_query_builder_timeseries", + params: { queries: [draft] }, + }), + ).toBeUndefined() + expect( + dataSourceRouteParams({ endpoint: "raw_sql_chart", params: { sql: "SELECT 1" } }), + ).toBeUndefined() + }) }) describe("isQueryDataSource", () => { diff --git a/packages/widgets/src/dashboard/access.ts b/packages/widgets/src/dashboard/access.ts index 7786b24be..bc2b39470 100644 --- a/packages/widgets/src/dashboard/access.ts +++ b/packages/widgets/src/dashboard/access.ts @@ -19,14 +19,25 @@ import type { QuerySet, QueryResultShape } from "@maple/query-model" const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value) -/** The 5 v2 endpoints that carried a user-authored query rather than a fixed route. */ -const QUERY_ENDPOINT_SHAPES: Record = { - custom_query_builder_timeseries: "timeseries", - custom_query_builder_breakdown: "breakdown", - custom_query_builder_list: "list", -} +/** + * The v2 endpoints that carried a user-authored query rather than a fixed route, + * keyed by the result shape that is the v3 identity of the same thing. + * + * Canonical in this direction because `construct.ts` writes it and the MCP + * inspector reports it; the endpoint → shape lookup below is derived, so the two + * cannot drift. + */ +export const QUERY_SHAPE_ENDPOINTS = { + timeseries: "custom_query_builder_timeseries", + breakdown: "custom_query_builder_breakdown", + list: "custom_query_builder_list", +} as const satisfies Record + +const QUERY_ENDPOINT_SHAPES: Record = Object.fromEntries( + Object.entries(QUERY_SHAPE_ENDPOINTS).map(([shape, endpoint]) => [endpoint, shape]), +) as Record -const RAW_SQL_ENDPOINT = "raw_sql_chart" +export const RAW_SQL_ENDPOINT = "raw_sql_chart" /** * The endpoint name, for consumers that still dispatch on it. @@ -121,11 +132,24 @@ export const dataSourceRawSql = (dataSource: unknown): RawSqlDataSource | null = } /** - * The opaque params bag, for the curated fixed-route endpoints that still have - * one. Returns undefined for a typed v3 arm — there is nothing opaque left. + * The opaque params bag of a curated fixed route. + * + * Route data sources keep an endpoint plus an untyped bag in v3 too — closing + * that bag is per-route and a much larger job — so this is the one accessor + * whose result stays opaque. Its caller is the widget editor's legacy fallback, + * which reads pre-query-builder widgets (`custom_timeseries` and friends). + * + * Returns undefined for a query-builder or raw-SQL endpoint even on v2, where + * the bag physically exists: those have typed accessors above, and returning the + * bag here would give v2 an answer v3 cannot give, which is the version + * asymmetry these accessors exist to prevent. */ export const dataSourceRouteParams = (dataSource: unknown): Record | undefined => { if (!isRecord(dataSource)) return undefined - if (typeof dataSource.kind === "string" && dataSource.kind !== "route") return undefined + if (typeof dataSource.kind === "string") { + if (dataSource.kind !== "route") return undefined + } else if (dataSourceQuerySet(dataSource) !== null || dataSourceRawSql(dataSource) !== null) { + return undefined + } return isRecord(dataSource.params) ? dataSource.params : undefined } diff --git a/packages/widgets/src/dashboard/construct.test.ts b/packages/widgets/src/dashboard/construct.test.ts new file mode 100644 index 000000000..d66fd71cd --- /dev/null +++ b/packages/widgets/src/dashboard/construct.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from "vitest" +import { dataSourceEndpoint, dataSourceQuerySet, dataSourceRawSql } from "./access" +import { makeQueryDataSource, makeRawSqlDataSource, makeRouteDataSource } from "./construct" + +/** + * The round trip is the whole contract: whatever a constructor writes, the + * matching accessor must read back unchanged. That property is what makes the + * v2 → v3 flip an edit to these two files rather than a sweep of every writer, + * so it is asserted per constructor rather than left to the version bump. + */ + +const draft = { id: "q1", name: "A", aggregation: "count", dataSource: "traces" } as const +const formula = { id: "f1", name: "F1", expression: "A / B", legend: "ratio" } + +describe("makeQueryDataSource", () => { + it("round-trips every result shape", () => { + for (const resultShape of ["timeseries", "breakdown", "list"] as const) { + const input = { resultShape, queries: [draft] } + expect(dataSourceQuerySet(makeQueryDataSource(input))).toEqual({ + ...input, + formulas: undefined, + comparison: undefined, + }) + } + }) + + it("round-trips formulas and a comparison window", () => { + const input = { + resultShape: "timeseries", + queries: [draft], + formulas: [formula], + comparison: { mode: "previous_period", includePercentChange: true }, + } as const + expect(dataSourceQuerySet(makeQueryDataSource(input))).toEqual(input) + }) + + it("omits formulas and comparison rather than writing empty keys", () => { + // A widget that never had formulas must not become indistinguishable from + // one that lost them on the next read-modify-write. + const params = makeQueryDataSource({ resultShape: "list", queries: [] }).params + expect(params).toEqual({ queries: [] }) + }) + + it("still writes the v2 endpoint while the stored version is 2", () => { + expect(dataSourceEndpoint(makeQueryDataSource({ resultShape: "breakdown", queries: [] }))).toBe( + "custom_query_builder_breakdown", + ) + }) + + it("carries a transform through untouched", () => { + const transform = { reduceToValue: { field: "value", aggregate: "first" } } as const + expect(makeQueryDataSource({ resultShape: "timeseries", queries: [], transform }).transform).toEqual( + transform, + ) + }) +}) + +describe("makeRawSqlDataSource", () => { + it("round-trips a full payload", () => { + const input = { sql: "SELECT 1", displayType: "line", granularitySeconds: 60 } + expect(dataSourceRawSql(makeRawSqlDataSource(input))).toEqual(input) + }) + + it("round-trips a bare SQL string", () => { + expect(dataSourceRawSql(makeRawSqlDataSource({ sql: "SELECT 1" }))).toEqual({ + sql: "SELECT 1", + displayType: undefined, + granularitySeconds: undefined, + }) + }) + + it("is not readable as a query set", () => { + expect(dataSourceQuerySet(makeRawSqlDataSource({ sql: "SELECT 1" }))).toBeNull() + }) +}) + +describe("makeRouteDataSource", () => { + it("round-trips through dataSourceEndpoint", () => { + expect(dataSourceEndpoint(makeRouteDataSource("service_overview", { limit: 5 }))).toBe( + "service_overview", + ) + }) + + it("is neither a query set nor raw SQL", () => { + const source = makeRouteDataSource("service_overview") + expect(dataSourceQuerySet(source)).toBeNull() + expect(dataSourceRawSql(source)).toBeNull() + }) + + it("omits an absent params bag rather than writing an empty one", () => { + expect(makeRouteDataSource("service_overview")).toEqual({ endpoint: "service_overview" }) + }) +}) diff --git a/packages/widgets/src/dashboard/construct.ts b/packages/widgets/src/dashboard/construct.ts new file mode 100644 index 000000000..1c038003d --- /dev/null +++ b/packages/widgets/src/dashboard/construct.ts @@ -0,0 +1,74 @@ +import type { QueryResultShape, QuerySet } from "@maple/query-model" +import { QUERY_SHAPE_ENDPOINTS, RAW_SQL_ENDPOINT, type RawSqlDataSource } from "./access" +import type { WidgetDataSourceTransformV2 } from "./shared/transform" +import type { WidgetDataSourceV2 } from "./v2/data-source" + +/** + * Writing a widget's data source without caring which schema version stores it. + * + * The mirror of `access.ts`. Those accessors let readers move off the raw + * `{ endpoint, params }` shape; these constructors do the same for writers — + * dashboard templates, the Perses importer and the MCP widget builders all hand + * a *meaning* ("a timeseries query set", "some raw SQL") to a function instead of + * hand-assembling an endpoint string and an untyped bag. + * + * While `CURRENT_DASHBOARD_SCHEMA_VERSION` is 2 these emit `{ endpoint, params }`. + * At the flip they emit the typed v3 union and no call site changes. The + * round-trip tests in `construct.test.ts` are what hold that promise: every + * constructor's output must read back through the matching accessor unchanged. + */ + +type WidgetDataSource = typeof WidgetDataSourceV2.Type +type WidgetDataSourceTransform = typeof WidgetDataSourceTransformV2.Type + +export interface QueryDataSourceInput extends QuerySet { + readonly resultShape: QueryResultShape + readonly transform?: WidgetDataSourceTransform +} + +/** A widget backed by a user-authored query set. */ +export const makeQueryDataSource = (input: QueryDataSourceInput): WidgetDataSource => ({ + endpoint: QUERY_SHAPE_ENDPOINTS[input.resultShape], + params: { + queries: input.queries, + // Absent rather than empty when the caller has none: `dataSourceQuerySet` + // reads both as "no formulas", and writing the empty key back would make a + // widget that never had formulas indistinguishable from one that lost them. + ...(input.formulas === undefined ? {} : { formulas: input.formulas }), + ...(input.comparison === undefined ? {} : { comparison: input.comparison }), + }, + ...(input.transform === undefined ? {} : { transform: input.transform }), +}) + +export interface RawSqlDataSourceInput extends RawSqlDataSource { + readonly transform?: WidgetDataSourceTransform +} + +/** A widget backed by user-authored ClickHouse SQL. */ +export const makeRawSqlDataSource = (input: RawSqlDataSourceInput): WidgetDataSource => ({ + endpoint: RAW_SQL_ENDPOINT, + params: { + sql: input.sql, + ...(input.displayType === undefined ? {} : { displayType: input.displayType }), + ...(input.granularitySeconds === undefined ? {} : { granularitySeconds: input.granularitySeconds }), + }, + ...(input.transform === undefined ? {} : { transform: input.transform }), +}) + +/** + * A widget backed by one of the curated fixed routes (`service_overview`, …). + * + * These keep an endpoint name and an opaque params bag in v3 too — the bag is + * per-route and closing it is a separate, much larger job — so this constructor + * exists for symmetry and to give the sweep one shape to grep for, not because + * the call site would otherwise break at the flip. + */ +export const makeRouteDataSource = ( + endpoint: string, + params?: Record, + transform?: WidgetDataSourceTransform, +): WidgetDataSource => ({ + endpoint, + ...(params === undefined ? {} : { params }), + ...(transform === undefined ? {} : { transform }), +}) diff --git a/packages/widgets/src/dashboard/index.ts b/packages/widgets/src/dashboard/index.ts index c3f5c7f5b..c8a5e7c6f 100644 --- a/packages/widgets/src/dashboard/index.ts +++ b/packages/widgets/src/dashboard/index.ts @@ -25,8 +25,17 @@ export { dataSourceRawSql, dataSourceRouteParams, isQueryDataSource, + QUERY_SHAPE_ENDPOINTS, + RAW_SQL_ENDPOINT, type RawSqlDataSource, } from "./access" +export { + makeQueryDataSource, + makeRawSqlDataSource, + makeRouteDataSource, + type QueryDataSourceInput, + type RawSqlDataSourceInput, +} from "./construct" export { type DashboardParseOutcome, parseStoredDashboard, stampCurrentVersion } from "./parse" export { CURRENT_DASHBOARD_SCHEMA_VERSION, DashboardSchemaVersion } from "./version" export { makeWidgetDisplayConfigSchema } from "./shared/display" diff --git a/packages/widgets/src/dashboard/migrations/index.ts b/packages/widgets/src/dashboard/migrations/index.ts index b62883bf7..d28862855 100644 --- a/packages/widgets/src/dashboard/migrations/index.ts +++ b/packages/widgets/src/dashboard/migrations/index.ts @@ -43,7 +43,7 @@ export const migrateToLatest = (document: unknown): Record => { // current. Decode fails either way — but stamped, the next writer persists the // lie and the original version is gone. Failing to decode a document we // genuinely cannot read is recoverable; corrupting it is not. - const declared = isPlainObject(document) ? document.schemaVersion : undefined + const declared = document.schemaVersion if (typeof declared === "number" && declared > CURRENT_DASHBOARD_SCHEMA_VERSION) { return document } From 44fa0fe998d923777a46d92cc1241b027180b30d Mon Sep 17 00:00:00 2001 From: Makisuo Date: Thu, 13 Aug 2026 21:01:24 +0200 Subject: [PATCH 05/13] e --- .../application/error-tracking.ts | 25 +- .../application/http-endpoints.ts | 12 +- .../application/metric-overview.ts | 31 +- .../application/platform-overview.ts | 36 +- .../application/service-health.ts | 31 +- .../application/top-errors.ts | 55 +- apps/api/src/mcp/lib/chart-statistics.ts | 5 +- apps/api/src/mcp/lib/inspect-widget.ts | 44 +- apps/api/src/mcp/tools/create-dashboard.ts | 23 +- apps/api/src/mcp/tools/query-data.ts | 8 +- .../api/src/services/alerts/AlertRuleModel.ts | 3 +- .../dashboards/perses-dashboard-import.ts | 4 +- apps/web/src/api/warehouse/custom-charts.ts | 7 +- apps/web/src/api/warehouse/metrics.ts | 3 +- .../api/warehouse/query-builder-timeseries.ts | 26 +- .../canvas/dashboard-canvas.tsx | 3 +- .../dashboard-builder/config/chart-picker.tsx | 16 +- .../config/settings-fields.tsx | 6 - .../config/widget-query-builder-page.tsx | 19 +- .../dashboard-builder/data-source-registry.ts | 3 + .../list/dashboard-summary.ts | 18 +- .../src/components/dashboard-builder/types.ts | 20 +- .../widgets/types/breakdown.tsx | 35 +- .../dashboard-builder/widgets/types/gauge.tsx | 5 +- .../dashboard-builder/widgets/types/list.tsx | 23 +- .../widgets/types/markdown.tsx | 3 +- .../dashboard-builder/widgets/types/stat.tsx | 5 +- .../dashboard-builder/widgets/types/table.tsx | 18 +- .../widgets/widget-actions-context.tsx | 12 +- .../widgets/widget-definitions.ts | 562 ++++++++---------- .../planetscale/planetscale-alert-menu.tsx | 9 +- .../metrics/metric-graduation-actions.tsx | 9 +- .../src/components/widget-lab/widget-lab.tsx | 3 +- .../hooks/use-metric-scoped-autocomplete.ts | 3 +- apps/web/src/hooks/use-widget-data.ts | 10 +- .../web/src/lib/alerts/widget-prefill.test.ts | 36 ++ apps/web/src/lib/alerts/widget-prefill.ts | 26 +- .../query-builder/widget-builder-shared.ts | 4 +- .../widget-builder-utils.test.ts | 3 +- .../lib/query-builder/widget-builder-utils.ts | 24 +- .../query-builder/widget-type-cycle.test.ts | 1 - apps/web/src/routes/metrics/$metricName.tsx | 3 +- apps/web/src/routes/metrics/index.tsx | 13 +- packages/query-model/src/query-model.test.ts | 16 + packages/query-model/src/series-reducer.ts | 11 + packages/widgets/src/dashboard/access.ts | 79 ++- packages/widgets/src/dashboard/construct.ts | 42 +- packages/widgets/src/dashboard/index.ts | 2 + 48 files changed, 700 insertions(+), 655 deletions(-) diff --git a/apps/api/src/dashboard-templates/application/error-tracking.ts b/apps/api/src/dashboard-templates/application/error-tracking.ts index faaadeaea..4ad239238 100644 --- a/apps/api/src/dashboard-templates/application/error-tracking.ts +++ b/apps/api/src/dashboard-templates/application/error-tracking.ts @@ -9,6 +9,7 @@ import { templateId, } from "@/dashboard-templates/helpers" import type { TemplateDefinition, WidgetDef } from "@/dashboard-templates/types" +import { makeRouteDataSource } from "@maple/widgets/dashboard" function widgets(serviceName?: string): WidgetDef[] { const where = serviceWhereClause(serviceName) @@ -33,13 +34,10 @@ function widgets(serviceName?: string): WidgetDef[] { { id: "errors-by-type", visualization: "table", - dataSource: { - endpoint: "errors_by_type", - params: { - ...(serviceName && { services: [serviceName] }), - limit: 20, - }, - }, + dataSource: makeRouteDataSource("errors_by_type", { + ...(serviceName && { services: [serviceName] }), + limit: 20, + }), display: { title: "Errors by Type", columns: [ @@ -53,14 +51,11 @@ function widgets(serviceName?: string): WidgetDef[] { { id: "recent-error-traces", visualization: "list", - dataSource: { - endpoint: "list_traces", - params: { - ...(serviceName && { service: serviceName }), - hasError: true, - limit: 10, - }, - }, + dataSource: makeRouteDataSource("list_traces", { + ...(serviceName && { service: serviceName }), + hasError: true, + limit: 10, + }), display: { title: "Recent Error Traces", listDataSource: "traces", diff --git a/apps/api/src/dashboard-templates/application/http-endpoints.ts b/apps/api/src/dashboard-templates/application/http-endpoints.ts index 9ac9accad..7844b22c8 100644 --- a/apps/api/src/dashboard-templates/application/http-endpoints.ts +++ b/apps/api/src/dashboard-templates/application/http-endpoints.ts @@ -10,6 +10,7 @@ import { templateId, } from "@/dashboard-templates/helpers" import type { TemplateDefinition, WidgetDef } from "@/dashboard-templates/types" +import { makeRouteDataSource } from "@maple/widgets/dashboard" function widgets(serviceName?: string): WidgetDef[] { const where = serviceWhereClause(serviceName) @@ -77,13 +78,10 @@ function widgets(serviceName?: string): WidgetDef[] { { id: "recent-traces", visualization: "list", - dataSource: { - endpoint: "list_traces", - params: { - ...(serviceName && { service: serviceName }), - limit: 10, - }, - }, + dataSource: makeRouteDataSource("list_traces", { + ...(serviceName && { service: serviceName }), + limit: 10, + }), display: { title: "Recent Traces", listDataSource: "traces", diff --git a/apps/api/src/dashboard-templates/application/metric-overview.ts b/apps/api/src/dashboard-templates/application/metric-overview.ts index 877dfe0c1..ec930a1a2 100644 --- a/apps/api/src/dashboard-templates/application/metric-overview.ts +++ b/apps/api/src/dashboard-templates/application/metric-overview.ts @@ -11,6 +11,7 @@ import { } from "@/dashboard-templates/helpers" import type { TemplateDefinition, WidgetDef } from "@/dashboard-templates/types" import { type QueryBuilderMetricType, toQueryBuilderMetricType } from "@maple/query-model" +import { makeRouteDataSource } from "@maple/widgets/dashboard" function widgets(opts: { metricName: string @@ -29,42 +30,42 @@ function widgets(opts: { { id: "metric-current", visualization: "stat", - dataSource: { - endpoint: "custom_timeseries", - params: { source: "metrics", metric: agg, groupBy: "none", filters: metricsFilters }, - transform: { + dataSource: makeRouteDataSource( + "custom_timeseries", + { source: "metrics", metric: agg, groupBy: "none", filters: metricsFilters }, + { flattenSeries: { valueField: "value" }, reduceToValue: { field: "value", aggregate: "avg" }, }, - }, + ), display: { title: `${opts.metricName} (${agg})` }, layout: { x: 0, y: 0, w: 4, h: 2 }, }, { id: "metric-max", visualization: "stat", - dataSource: { - endpoint: "custom_timeseries", - params: { source: "metrics", metric: "max", groupBy: "none", filters: metricsFilters }, - transform: { + dataSource: makeRouteDataSource( + "custom_timeseries", + { source: "metrics", metric: "max", groupBy: "none", filters: metricsFilters }, + { flattenSeries: { valueField: "value" }, reduceToValue: { field: "value", aggregate: "max" }, }, - }, + ), display: { title: `${opts.metricName} (max)` }, layout: { x: 4, y: 0, w: 4, h: 2 }, }, { id: "metric-count", visualization: "stat", - dataSource: { - endpoint: "custom_timeseries", - params: { source: "metrics", metric: "count", groupBy: "none", filters: metricsFilters }, - transform: { + dataSource: makeRouteDataSource( + "custom_timeseries", + { source: "metrics", metric: "count", groupBy: "none", filters: metricsFilters }, + { flattenSeries: { valueField: "value" }, reduceToValue: { field: "value", aggregate: "sum" }, }, - }, + ), display: { title: "Data Points", unit: "number" }, layout: { x: 8, y: 0, w: 4, h: 2 }, }, diff --git a/apps/api/src/dashboard-templates/application/platform-overview.ts b/apps/api/src/dashboard-templates/application/platform-overview.ts index d717a63f3..82590d063 100644 --- a/apps/api/src/dashboard-templates/application/platform-overview.ts +++ b/apps/api/src/dashboard-templates/application/platform-overview.ts @@ -7,53 +7,50 @@ import { templateId, } from "@/dashboard-templates/helpers" import type { TemplateDefinition, WidgetDef } from "@/dashboard-templates/types" +import { makeRouteDataSource } from "@maple/widgets/dashboard" function widgets(): WidgetDef[] { return [ { id: "total-throughput", visualization: "stat", - dataSource: { - endpoint: "service_usage", - transform: { reduceToValue: { field: "totalTraceCount", aggregate: "sum" } }, - }, + dataSource: makeRouteDataSource("service_usage", undefined, { + reduceToValue: { field: "totalTraceCount", aggregate: "sum" }, + }), display: { title: "Total Traces", unit: "number" }, layout: { x: 0, y: 0, w: 3, h: 2 }, }, { id: "total-errors", visualization: "stat", - dataSource: { - endpoint: "errors_summary", - transform: { reduceToValue: { field: "totalErrors", aggregate: "first" } }, - }, + dataSource: makeRouteDataSource("errors_summary", undefined, { + reduceToValue: { field: "totalErrors", aggregate: "first" }, + }), display: { title: "Total Errors", unit: "number" }, layout: { x: 3, y: 0, w: 3, h: 2 }, }, { id: "error-rate", visualization: "stat", - dataSource: { - endpoint: "errors_summary", - transform: { reduceToValue: { field: "errorRate", aggregate: "first" } }, - }, + dataSource: makeRouteDataSource("errors_summary", undefined, { + reduceToValue: { field: "errorRate", aggregate: "first" }, + }), display: { title: "Error Rate", unit: "percent" }, layout: { x: 6, y: 0, w: 3, h: 2 }, }, { id: "affected-services", visualization: "stat", - dataSource: { - endpoint: "errors_summary", - transform: { reduceToValue: { field: "affectedServicesCount", aggregate: "first" } }, - }, + dataSource: makeRouteDataSource("errors_summary", undefined, { + reduceToValue: { field: "affectedServicesCount", aggregate: "first" }, + }), display: { title: "Affected Services", unit: "number" }, layout: { x: 9, y: 0, w: 3, h: 2 }, }, { id: "service-overview", visualization: "table", - dataSource: { endpoint: "service_overview" }, + dataSource: makeRouteDataSource("service_overview"), display: { title: "Service Overview", columns: [ @@ -98,10 +95,7 @@ function widgets(): WidgetDef[] { { id: "recent-error-traces", visualization: "list", - dataSource: { - endpoint: "list_traces", - params: { hasError: true, limit: 10 }, - }, + dataSource: makeRouteDataSource("list_traces", { hasError: true, limit: 10 }), display: { title: "Recent Error Traces", listDataSource: "traces", diff --git a/apps/api/src/dashboard-templates/application/service-health.ts b/apps/api/src/dashboard-templates/application/service-health.ts index 6e2424ba6..19b3bb74a 100644 --- a/apps/api/src/dashboard-templates/application/service-health.ts +++ b/apps/api/src/dashboard-templates/application/service-health.ts @@ -10,6 +10,7 @@ import { templateId, } from "@/dashboard-templates/helpers" import type { TemplateDefinition, WidgetDef } from "@/dashboard-templates/types" +import { makeRouteDataSource } from "@maple/widgets/dashboard" function widgets(serviceName?: string): WidgetDef[] { const where = serviceWhereClause(serviceName) @@ -18,11 +19,11 @@ function widgets(serviceName?: string): WidgetDef[] { { id: "throughput", visualization: "stat", - dataSource: { - endpoint: "service_overview", - params: serviceName ? { service_name: serviceName } : {}, - transform: { reduceToValue: { field: "throughput", aggregate: "sum" } }, - }, + dataSource: makeRouteDataSource( + "service_overview", + serviceName ? { service_name: serviceName } : {}, + { reduceToValue: { field: "throughput", aggregate: "sum" } }, + ), display: { title: "Throughput", unit: "number" }, layout: { x: 0, y: 0, w: 3, h: 2 }, }, @@ -48,22 +49,22 @@ function widgets(serviceName?: string): WidgetDef[] { { id: "p50", visualization: "stat", - dataSource: { - endpoint: "service_overview", - params: serviceName ? { service_name: serviceName } : {}, - transform: { reduceToValue: { field: "p50LatencyMs", aggregate: "avg" } }, - }, + dataSource: makeRouteDataSource( + "service_overview", + serviceName ? { service_name: serviceName } : {}, + { reduceToValue: { field: "p50LatencyMs", aggregate: "avg" } }, + ), display: { title: "P50 Latency", unit: "duration_ms" }, layout: { x: 6, y: 0, w: 3, h: 2 }, }, { id: "p95", visualization: "stat", - dataSource: { - endpoint: "service_overview", - params: serviceName ? { service_name: serviceName } : {}, - transform: { reduceToValue: { field: "p95LatencyMs", aggregate: "avg" } }, - }, + dataSource: makeRouteDataSource( + "service_overview", + serviceName ? { service_name: serviceName } : {}, + { reduceToValue: { field: "p95LatencyMs", aggregate: "avg" } }, + ), display: { title: "P95 Latency", unit: "duration_ms" }, layout: { x: 9, y: 0, w: 3, h: 2 }, }, diff --git a/apps/api/src/dashboard-templates/application/top-errors.ts b/apps/api/src/dashboard-templates/application/top-errors.ts index e7f347a84..14f7d6c1f 100644 --- a/apps/api/src/dashboard-templates/application/top-errors.ts +++ b/apps/api/src/dashboard-templates/application/top-errors.ts @@ -9,6 +9,7 @@ import { templateId, } from "@/dashboard-templates/helpers" import type { TemplateDefinition, WidgetDef } from "@/dashboard-templates/types" +import { makeRouteDataSource } from "@maple/widgets/dashboard" function widgets(serviceName?: string): WidgetDef[] { const where = serviceWhereClause(serviceName) @@ -16,46 +17,43 @@ function widgets(serviceName?: string): WidgetDef[] { { id: "total-errors", visualization: "stat", - dataSource: { - endpoint: "errors_summary", - params: serviceName ? { services: [serviceName] } : {}, - transform: { reduceToValue: { field: "totalErrors", aggregate: "first" } }, - }, + dataSource: makeRouteDataSource( + "errors_summary", + serviceName ? { services: [serviceName] } : {}, + { reduceToValue: { field: "totalErrors", aggregate: "first" } }, + ), display: { title: "Total Errors", unit: "number" }, layout: { x: 0, y: 0, w: 4, h: 2 }, }, { id: "error-rate", visualization: "stat", - dataSource: { - endpoint: "errors_summary", - params: serviceName ? { services: [serviceName] } : {}, - transform: { reduceToValue: { field: "errorRate", aggregate: "first" } }, - }, + dataSource: makeRouteDataSource( + "errors_summary", + serviceName ? { services: [serviceName] } : {}, + { reduceToValue: { field: "errorRate", aggregate: "first" } }, + ), display: { title: "Error Rate", unit: "percent" }, layout: { x: 4, y: 0, w: 4, h: 2 }, }, { id: "affected-services", visualization: "stat", - dataSource: { - endpoint: "errors_summary", - params: serviceName ? { services: [serviceName] } : {}, - transform: { reduceToValue: { field: "affectedServicesCount", aggregate: "first" } }, - }, + dataSource: makeRouteDataSource( + "errors_summary", + serviceName ? { services: [serviceName] } : {}, + { reduceToValue: { field: "affectedServicesCount", aggregate: "first" } }, + ), display: { title: "Affected Services", unit: "number" }, layout: { x: 8, y: 0, w: 4, h: 2 }, }, { id: "errors-by-type", visualization: "table", - dataSource: { - endpoint: "errors_by_type", - params: { - ...(serviceName && { services: [serviceName] }), - limit: 20, - }, - }, + dataSource: makeRouteDataSource("errors_by_type", { + ...(serviceName && { services: [serviceName] }), + limit: 20, + }), display: { title: "Errors by Type", columns: [ @@ -85,14 +83,11 @@ function widgets(serviceName?: string): WidgetDef[] { { id: "recent-error-traces", visualization: "list", - dataSource: { - endpoint: "list_traces", - params: { - ...(serviceName && { service: serviceName }), - hasError: true, - limit: 10, - }, - }, + dataSource: makeRouteDataSource("list_traces", { + ...(serviceName && { service: serviceName }), + hasError: true, + limit: 10, + }), display: { title: "Recent Error Traces", listDataSource: "traces", diff --git a/apps/api/src/mcp/lib/chart-statistics.ts b/apps/api/src/mcp/lib/chart-statistics.ts index 891470f95..df8df8bc0 100644 --- a/apps/api/src/mcp/lib/chart-statistics.ts +++ b/apps/api/src/mcp/lib/chart-statistics.ts @@ -1,4 +1,5 @@ import type { BreakdownItem, TimeseriesPoint } from "@maple/domain" +import type { QueryBuilderDataSource, QueryResultShape } from "@maple/query-model" export type ChartFlag = | "EMPTY" @@ -183,8 +184,8 @@ export function computeBreakdownStats(rows: ReadonlyArray): Query export interface FlagContext { metric?: string - source?: "traces" | "logs" | "metrics" - kind?: "timeseries" | "breakdown" + source?: QueryBuilderDataSource + kind?: Extract displayUnit?: string /** * True when the series is a numeric span-attribute aggregation (p95 of diff --git a/apps/api/src/mcp/lib/inspect-widget.ts b/apps/api/src/mcp/lib/inspect-widget.ts index 3771866ff..550b78d1c 100644 --- a/apps/api/src/mcp/lib/inspect-widget.ts +++ b/apps/api/src/mcp/lib/inspect-widget.ts @@ -35,31 +35,23 @@ import type { WidgetInspectionSummary, WidgetInspectionVerdict, } from "@maple/domain" -import { dataSourceEndpoint, dataSourceQuerySet, dataSourceRawSql } from "@maple/widgets/dashboard" +import { + dataSourceEndpoint, + dataSourceQuerySet, + dataSourceRawSql, + dataSourceTransform, + QUERY_SHAPE_ENDPOINTS, + RAW_SQL_ENDPOINT, +} from "@maple/widgets/dashboard" import type { TenantContext } from "@/services/auth/tenant-context" -/** - * The label a raw-SQL inspection reports, NOT a dispatch key — dispatch goes - * through `dataSourceRawSql`, which reads v2 and v3 alike. This name is part of - * the MCP response contract, so it stays spelled the v2 way even once the stored - * data source no longer has an endpoint. - */ -const RAW_SQL_ENDPOINT = "raw_sql_chart" - -/** - * The legacy endpoint name for a query result shape. - * - * `inspect_chart_data` reports `endpoint` as part of its response contract, but - * a v3 `kind: "query"` data source has no endpoint — the shape is the identity. - * Synthesising the v2 name here keeps the MCP payload stable for agents that - * already branch on it. This is the ONLY place a legacy endpoint name is - * produced from a typed data source; dispatch never goes through it. - */ -const QUERY_SHAPE_ENDPOINTS = { - timeseries: "custom_query_builder_timeseries", - breakdown: "custom_query_builder_breakdown", - list: "custom_query_builder_list", -} as const +// `RAW_SQL_ENDPOINT` and `QUERY_SHAPE_ENDPOINTS` are used here as LABELS, not as +// dispatch keys — dispatch goes through `dataSourceRawSql` / `dataSourceQuerySet`, +// which read v2 and v3 alike. `inspect_chart_data` reports `endpoint` as part of +// its response contract and a v3 `kind: "query"` data source has no endpoint, so +// the shape is mapped back onto the legacy name to keep the MCP payload stable +// for agents that already branch on it. Imported rather than re-declared so a +// fourth result shape cannot compile in `access.ts` while silently missing here. const MAX_QUERIES = 5 // Rows captured for a raw-SQL widget inspection — enough to spot-check, capped @@ -459,10 +451,8 @@ export const inspectWidget = Effect.fn("inspectWidget")( const formulas = decodedParams.formulas ?? [] const hasFormulaWarning = formulas.length > 0 - const transformObj = widget.dataSource.transform as Record | undefined - const reduceToValue = transformObj?.reduceToValue as - | { field?: unknown; aggregate?: unknown } - | undefined + const transformObj = dataSourceTransform(widget.dataSource) + const reduceToValue = transformObj?.reduceToValue const hasUnsupportedTransform = transformObj !== undefined && Object.keys(transformObj).some((k) => k !== "reduceToValue") diff --git a/apps/api/src/mcp/tools/create-dashboard.ts b/apps/api/src/mcp/tools/create-dashboard.ts index ee0cf1b81..901045081 100644 --- a/apps/api/src/mcp/tools/create-dashboard.ts +++ b/apps/api/src/mcp/tools/create-dashboard.ts @@ -30,6 +30,7 @@ import { import type { TemplateParameterValues, WidgetDef } from "@/dashboard-templates" import { validateDashboardTimeRange } from "@/mcp/lib/resolve-dashboard-time-range" import { MAX_LIST_RANGE_SECONDS, MAX_QUERY_RANGE_SECONDS, formatRangeSeconds } from "@maple/query-engine" +import { makeRouteDataSource } from "@maple/widgets/dashboard" const decodePortableDashboard = Schema.decodeUnknownEffect(PortableDashboardDocument) const PortableDashboardFromJson = Schema.fromJsonString(PortableDashboardDocument) @@ -172,13 +173,10 @@ function simpleSpecToWidget( return { id, visualization: viz, - dataSource: { - endpoint: "list_logs", - params: { - ...(spec.service_name && { service: spec.service_name }), - limit: 10, - }, - }, + dataSource: makeRouteDataSource("list_logs", { + ...(spec.service_name && { service: spec.service_name }), + limit: 10, + }), display: { title: spec.title, listDataSource: "logs", listLimit: 10 }, layout, } @@ -186,13 +184,10 @@ function simpleSpecToWidget( return { id, visualization: viz, - dataSource: { - endpoint: "list_traces", - params: { - ...(spec.service_name && { service: spec.service_name }), - limit: 10, - }, - }, + dataSource: makeRouteDataSource("list_traces", { + ...(spec.service_name && { service: spec.service_name }), + limit: 10, + }), display: { title: spec.title, listDataSource: "traces", listLimit: 10 }, layout, } diff --git a/apps/api/src/mcp/tools/query-data.ts b/apps/api/src/mcp/tools/query-data.ts index 2f742af45..44f0f2d4d 100644 --- a/apps/api/src/mcp/tools/query-data.ts +++ b/apps/api/src/mcp/tools/query-data.ts @@ -35,6 +35,7 @@ import { SpanName, type QueryDataQueryContext, } from "@maple/domain" +import { QUERY_BUILDER_DATA_SOURCES, type QueryResultShape } from "@maple/query-model" const asServiceName = Schema.decodeUnknownSync(ServiceName) const asSpanName = Schema.decodeUnknownSync(SpanName) @@ -43,13 +44,16 @@ const asCommitSha = Schema.decodeUnknownSync(CommitSha) const asMetricName = Schema.decodeUnknownSync(MetricName) const queryDataSchema = Schema.Struct({ - source: Schema.Literals(["traces", "logs", "metrics"]).annotate({ + source: Schema.Literals(QUERY_BUILDER_DATA_SOURCES).annotate({ description: "Data source. Use 'traces' for request/span analysis (latency, errors, throughput). " + "Use 'logs' for log volume analysis. " + "Use 'metrics' for custom metric aggregation (requires metric_name and metric_type — call list_metrics first).", }), - kind: Schema.Literals(["timeseries", "breakdown"]).annotate({ + kind: Schema.Literals([ + "timeseries", + "breakdown", + ] as const satisfies ReadonlyArray).annotate({ description: "Query shape. Use 'timeseries' when the user asks about trends, patterns, or 'how has X changed over time'. " + "Use 'breakdown' when asking about top-N, distribution, or 'which services have the most errors'. " + diff --git a/apps/api/src/services/alerts/AlertRuleModel.ts b/apps/api/src/services/alerts/AlertRuleModel.ts index a828dea3e..9d6a822a9 100644 --- a/apps/api/src/services/alerts/AlertRuleModel.ts +++ b/apps/api/src/services/alerts/AlertRuleModel.ts @@ -34,6 +34,7 @@ import type { AlertRuleRow } from "@maple/db" import { Array as Arr, Effect, Option, Result, Schema } from "effect" import { dateToMs, msToDate } from "@/platform/time" import type { AlertRuntimeShape } from "./AlertRuntime" +import type { QueryBuilderDataSource } from "@maple/query-model" const StringArraySchema = Schema.Array(Schema.String) const DestinationIdArraySchema = Schema.Array(AlertDestinationDocument.fields.id) @@ -217,7 +218,7 @@ export const compileRulePlan = Effect.fn("AlertsService.compileRulePlan")(functi } const resolveRuleGroupBy = ( - source: "traces" | "logs" | "metrics", + source: QueryBuilderDataSource, ): Effect.Effect< { readonly tokens: ReadonlyArray diff --git a/apps/api/src/services/dashboards/perses-dashboard-import.ts b/apps/api/src/services/dashboards/perses-dashboard-import.ts index fb30be80d..36f051fdb 100644 --- a/apps/api/src/services/dashboards/perses-dashboard-import.ts +++ b/apps/api/src/services/dashboards/perses-dashboard-import.ts @@ -10,7 +10,7 @@ import { widgetTypeByVisualization, type WidgetVisualization, } from "@maple/domain/http" -import { makeRawSqlDataSource } from "@maple/widgets/dashboard" +import { makeRawSqlDataSource, makeRouteDataSource } from "@maple/widgets/dashboard" type UnknownRecord = Record type DashboardWidget = typeof DashboardWidgetSchema.Type @@ -293,7 +293,7 @@ function rawSqlDataSource(args: { } function markdownDataSource(): DashboardWidget["dataSource"] { - return { endpoint: "markdown_static" } + return makeRouteDataSource("markdown_static") } function markdownWidgetContent(args: { diff --git a/apps/web/src/api/warehouse/custom-charts.ts b/apps/web/src/api/warehouse/custom-charts.ts index b2d1ca202..83b4c8d05 100644 --- a/apps/web/src/api/warehouse/custom-charts.ts +++ b/apps/web/src/api/warehouse/custom-charts.ts @@ -34,6 +34,7 @@ import { } from "@/api/warehouse/effect-utils" import { MapleApiAtomClient } from "@/lib/services/common/atom-client" import type { ServiceDetailTimeSeriesPoint, ServiceTimeSeriesPoint } from "@/api/warehouse/services" +import { QUERY_BUILDER_DATA_SOURCES, QUERY_BUILDER_METRIC_TYPES } from "@maple/query-model" const dateTimeString = WarehouseDateTimeString const asMetricName = Schema.decodeUnknownSync(MetricName) @@ -222,7 +223,7 @@ const SharedFiltersSchema = Schema.Struct({ spanName: Schema.optional(SpanName), severity: Schema.optional(Schema.String), metricName: Schema.optional(MetricName), - metricType: Schema.optional(Schema.Literals(["sum", "gauge", "histogram", "exponential_histogram"])), + metricType: Schema.optional(Schema.Literals(QUERY_BUILDER_METRIC_TYPES)), rootSpansOnly: Schema.optional(Schema.Boolean), environments: Schema.optional(Schema.mutable(Schema.Array(DeploymentEnvironment))), namespaces: Schema.optional(Schema.mutable(Schema.Array(ServiceNamespace))), @@ -254,7 +255,7 @@ const SharedFiltersSchema = Schema.Struct({ }) const CustomChartTimeSeriesInputSchema = Schema.Struct({ - source: Schema.Literals(["traces", "logs", "metrics"]), + source: Schema.Literals(QUERY_BUILDER_DATA_SOURCES), metric: Schema.String, groupBy: Schema.optional( Schema.Literals([ @@ -444,7 +445,7 @@ const getCustomChartTimeSeriesEffect = Effect.fn("QueryEngine.getCustomChartTime }) const CustomChartBreakdownInputSchema = Schema.Struct({ - source: Schema.Literals(["traces", "logs", "metrics"]), + source: Schema.Literals(QUERY_BUILDER_DATA_SOURCES), metric: Schema.String, groupBy: Schema.Literals(["service", "span_name", "status_code", "http_method", "severity", "attribute"]), filters: Schema.optional(SharedFiltersSchema), diff --git a/apps/web/src/api/warehouse/metrics.ts b/apps/web/src/api/warehouse/metrics.ts index 2f36c1b0e..9f1b29813 100644 --- a/apps/web/src/api/warehouse/metrics.ts +++ b/apps/web/src/api/warehouse/metrics.ts @@ -9,8 +9,9 @@ import { extractAttributeValues, runWarehouseQuery, } from "@/api/warehouse/effect-utils" +import { QUERY_BUILDER_METRIC_TYPES } from "@maple/query-model" -const MetricTypeSchema = Schema.Literals(["sum", "gauge", "histogram", "exponential_histogram"]) +const MetricTypeSchema = Schema.Literals(QUERY_BUILDER_METRIC_TYPES) const ListMetricsInputSchema = Schema.Struct({ limit: Schema.optional( diff --git a/apps/web/src/api/warehouse/query-builder-timeseries.ts b/apps/web/src/api/warehouse/query-builder-timeseries.ts index eedfc7a7b..de8691d03 100644 --- a/apps/web/src/api/warehouse/query-builder-timeseries.ts +++ b/apps/web/src/api/warehouse/query-builder-timeseries.ts @@ -13,6 +13,11 @@ import { type TimeseriesPoint, } from "@/components/query-builder/formula-results" import { QueryBuilderQueryDraftSchema } from "@maple/domain/http" +import { + QueryBuilderFormulaSchema, + type QueryComparisonMode, + QueryComparisonSchema, +} from "@maple/query-model" import { buildTimeseriesQuerySpec } from "@maple/query-engine/query-builder" import { decodeInput, @@ -28,27 +33,12 @@ type ExecuteError = WarehouseApiError | BackendError const dateTimeString = Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)) -const COMPARISON_MODES = ["none", "previous_period"] as const - const DEFAULT_STRATEGY = { enableEmptyRangeFallback: true, fallbackWindowSeconds: [24 * 60 * 60, 7 * 24 * 60 * 60, 31 * 24 * 60 * 60], maxFallbackRangeSeconds: 31 * 24 * 60 * 60, } as const -const FormulaSchema = Schema.Struct({ - id: Schema.String, - name: Schema.String, - expression: Schema.String, - legend: Schema.String, - hidden: Schema.optionalKey(Schema.Boolean), -}) - -const ComparisonSchema = Schema.Struct({ - mode: Schema.optional(Schema.Literals(COMPARISON_MODES)), - includePercentChange: Schema.optional(Schema.Boolean), -}) - const StrategySchema = Schema.Struct({ enableEmptyRangeFallback: Schema.optional(Schema.Boolean), fallbackWindowSeconds: Schema.optional( @@ -61,8 +51,8 @@ const QueryBuilderTimeseriesInputSchema = Schema.Struct({ startTime: dateTimeString, endTime: dateTimeString, queries: Schema.mutable(Schema.Array(QueryBuilderQueryDraftSchema)), - formulas: Schema.optional(Schema.mutable(Schema.Array(FormulaSchema))), - comparison: Schema.optional(ComparisonSchema), + formulas: Schema.optional(Schema.mutable(Schema.Array(QueryBuilderFormulaSchema))), + comparison: Schema.optional(QueryComparisonSchema), strategy: Schema.optional(StrategySchema), debug: Schema.optional(Schema.Boolean), }) @@ -93,7 +83,7 @@ interface QueryBuilderTimeseriesDebug { endTime: string } comparison: { - mode: "none" | "previous_period" + mode: QueryComparisonMode includePercentChange: boolean shiftedByMs: number previousStartTime: string | null diff --git a/apps/web/src/components/dashboard-builder/canvas/dashboard-canvas.tsx b/apps/web/src/components/dashboard-builder/canvas/dashboard-canvas.tsx index 6599d5d70..f8a864254 100644 --- a/apps/web/src/components/dashboard-builder/canvas/dashboard-canvas.tsx +++ b/apps/web/src/components/dashboard-builder/canvas/dashboard-canvas.tsx @@ -1,4 +1,5 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react" +import { dataSourceTransform } from "@maple/widgets/dashboard" import { GridLayout, noCompactor, verticalCompactor } from "react-grid-layout" import type { Layout } from "react-grid-layout" import "react-grid-layout/css/styles.css" @@ -94,7 +95,7 @@ const WidgetRenderer = memo(function WidgetRenderer({ widget }: { widget: Dashbo dataState={dataState} display={widget.display} mode={mode} - rowLimit={widget.dataSource.transform?.limit} + rowLimit={dataSourceTransform(widget.dataSource)?.limit} /> diff --git a/apps/web/src/components/dashboard-builder/config/chart-picker.tsx b/apps/web/src/components/dashboard-builder/config/chart-picker.tsx index eb17b9983..a47c46228 100644 --- a/apps/web/src/components/dashboard-builder/config/chart-picker.tsx +++ b/apps/web/src/components/dashboard-builder/config/chart-picker.tsx @@ -17,6 +17,7 @@ import type { import type { WidgetPresetDefinition } from "@/components/dashboard-builder/widgets/widget-definitions" import { widgetTypeList } from "@/components/dashboard-builder/widgets/types" import { createQueryDraft } from "@maple/query-engine/query-builder" +import { makeQueryDataSource } from "@maple/widgets/dashboard" import { deriveDefaultWidgetTitle } from "@/lib/query-builder/widget-builder-utils" // "Add widget". @@ -136,15 +137,12 @@ export function WidgetPicker({ open, onOpenChange, onSelect }: WidgetPickerProps const draft = createQueryDraft(0) const added = onSelect( "chart", - { - endpoint: "custom_query_builder_timeseries", - params: { - queries: [draft], - formulas: [], - comparison: { mode: "none", includePercentChange: true }, - debug: false, - }, - }, + makeQueryDataSource({ + resultShape: "timeseries", + queries: [draft], + formulas: [], + comparison: { mode: "none", includePercentChange: true }, + }), // Derived title ("Error rate by service.name") so freshly added // charts never render as "Untitled". { chartId, title: deriveDefaultWidgetTitle([draft]) }, diff --git a/apps/web/src/components/dashboard-builder/config/settings-fields.tsx b/apps/web/src/components/dashboard-builder/config/settings-fields.tsx index 089a301c0..4270b702d 100644 --- a/apps/web/src/components/dashboard-builder/config/settings-fields.tsx +++ b/apps/web/src/components/dashboard-builder/config/settings-fields.tsx @@ -581,12 +581,6 @@ function QueryOptions() { disabled={state.comparisonMode === "none"} onChange={(includePercentChange) => set({ includePercentChange })} /> - set({ debug })} - /> ) diff --git a/apps/web/src/components/dashboard-builder/config/widget-query-builder-page.tsx b/apps/web/src/components/dashboard-builder/config/widget-query-builder-page.tsx index b4d6e6b94..156cca075 100644 --- a/apps/web/src/components/dashboard-builder/config/widget-query-builder-page.tsx +++ b/apps/web/src/components/dashboard-builder/config/widget-query-builder-page.tsx @@ -1,6 +1,6 @@ import * as React from "react" import { widgetTypeByVisualization } from "@maple/domain/http" -import { dataSourceRawSql } from "@maple/widgets/dashboard" +import { dataSourceRawSql, dataSourceTransform, makeRawSqlDataSource } from "@maple/widgets/dashboard" import { Button } from "@maple/ui/components/ui/button" import { Tabs, TabsList, TabsTrigger } from "@maple/ui/components/ui/tabs" @@ -101,7 +101,7 @@ function buildRawSqlDataSource( // Stat and gauge both render a scalar, so they need a reduceToValue transform // for the widget to read `data[0].value`. If the user already set a transform // on the widget, keep theirs; otherwise inject the default. - const existingTransform = widget.dataSource.transform + const existingTransform = dataSourceTransform(widget.dataSource) const needsScalar = widgetTypeByVisualization(visualization)?.isScalar === true const transform = needsScalar && !existingTransform?.reduceToValue @@ -111,15 +111,12 @@ function buildRawSqlDataSource( } : existingTransform - return { - endpoint: "raw_sql_chart", - params: { - sql: draft.sql, - displayType, - ...(draft.granularitySeconds != null ? { granularitySeconds: draft.granularitySeconds } : {}), - }, - ...(transform ? { transform } : {}), - } + return makeRawSqlDataSource({ + sql: draft.sql, + displayType, + ...(draft.granularitySeconds == null ? {} : { granularitySeconds: draft.granularitySeconds }), + ...(transform === undefined ? {} : { transform }), + }) } export function WidgetQueryBuilderPage({ diff --git a/apps/web/src/components/dashboard-builder/data-source-registry.ts b/apps/web/src/components/dashboard-builder/data-source-registry.ts index 69643f22d..981c96840 100644 --- a/apps/web/src/components/dashboard-builder/data-source-registry.ts +++ b/apps/web/src/components/dashboard-builder/data-source-registry.ts @@ -128,6 +128,9 @@ export function toWidgetRequest( queries: querySet.queries, ...(querySet.formulas === undefined ? {} : { formulas: querySet.formulas }), ...(querySet.comparison === undefined ? {} : { comparison: querySet.comparison }), + ...(querySet.defaultLimit === undefined ? {} : { defaultLimit: querySet.defaultLimit }), + ...(querySet.limit === undefined ? {} : { limit: querySet.limit }), + ...(querySet.columns === undefined ? {} : { columns: querySet.columns }), }, } } diff --git a/apps/web/src/components/dashboard-builder/list/dashboard-summary.ts b/apps/web/src/components/dashboard-builder/list/dashboard-summary.ts index c7fa4f5e9..98cbedd7f 100644 --- a/apps/web/src/components/dashboard-builder/list/dashboard-summary.ts +++ b/apps/web/src/components/dashboard-builder/list/dashboard-summary.ts @@ -62,6 +62,12 @@ const DRAFT_SOURCE_DOMAIN: Record = { metrics: "Metrics", } +/** + * What an empty query widget claims to read. Matches `createQueryDraft`'s own + * default source, so the lane agrees with the first draft the editor will make. + */ +const DEFAULT_QUERY_DOMAIN: DashboardDomain = "Traces" + export type DashboardDomain = "Traces" | "Logs" | "Errors" | "Metrics" | "Raw SQL" /** Display order, so two dashboards reading the same signals label identically. */ @@ -75,7 +81,7 @@ const widgetDomains = (dataSource: unknown): ReadonlyArray => { if (dataSourceRawSql(dataSource) !== null) return ["Raw SQL"] const querySet = dataSourceQuerySet(dataSource) - if (querySet !== null) { + if (querySet !== null && querySet.queries.length > 0) { const domains: DashboardDomain[] = [] for (const query of querySet.queries) { const domain = DRAFT_SOURCE_DOMAIN[query?.dataSource] @@ -84,9 +90,15 @@ const widgetDomains = (dataSource: unknown): ReadonlyArray => { return domains } + // Falls through for a query widget with no drafts yet — a chart freshly + // dropped on the canvas. Its shape still says which signal it will read, and + // reporting nothing would make a board of new charts read "static only". const endpoint = dataSourceEndpoint(dataSource) - const domain = endpoint === null ? undefined : ENDPOINT_DOMAIN[endpoint] - return domain ? [domain] : [] + if (endpoint !== null) { + const domain = ENDPOINT_DOMAIN[endpoint] + if (domain) return [domain] + } + return querySet === null ? [] : [DEFAULT_QUERY_DOMAIN] } export const dashboardDomains = (dashboard: Dashboard): ReadonlyArray => { diff --git a/apps/web/src/components/dashboard-builder/types.ts b/apps/web/src/components/dashboard-builder/types.ts index 15f3391c9..8c9f002c2 100644 --- a/apps/web/src/components/dashboard-builder/types.ts +++ b/apps/web/src/components/dashboard-builder/types.ts @@ -13,13 +13,23 @@ import type { // The domain schemas decode to deeply-readonly types; web widgets are mutable // React/builder state, so the derived types are unwrapped to mutable form. +// +// `dataSource` is the one exception, wherever it appears (a widget's own, and +// the one `display.sparkline` embeds). A data source is always replaced +// wholesale, never edited field-by-field, so it stays readonly — which is what +// lets the constructors in `@maple/widgets/dashboard` be assigned straight into +// these types instead of every call site fighting a variance mismatch. type DeepMutable = T extends ReadonlyArray ? Array> : T extends object - ? { -readonly [K in keyof T]: DeepMutable } + ? { -readonly [K in keyof T]: K extends "dataSource" ? T[K] : DeepMutable } : T +// Deliberately NOT `@maple/query-model`'s `TimeRange`, which brands its ISO +// strings. Every producer in the UI (the time-range picker, the builder form) +// deals in plain strings; branding happens once, at the store's document +// boundary — the same reason `DashboardWidget["timeRange"]` is re-typed below. export type TimeRange = | { type: "relative"; value: string } | { type: "absolute"; startTime: string; endTime: string } @@ -53,7 +63,13 @@ export type DataSourceEndpoint = // `endpoint` is narrowed to the registry key union so the data-source registry // stays statically indexable; everything else comes straight from the schema. -export type WidgetDataSource = Omit, "endpoint"> & { +// +// Deliberately NOT `DeepMutable`, unlike the display/layout aliases below. A +// data source is replaced wholesale — the builder never edits one field of it in +// place — and keeping it readonly is what lets the constructors in +// `@maple/widgets/dashboard` (which return the schema type) be assigned here +// directly, instead of every call site fighting a variance mismatch. +export type WidgetDataSource = Omit & { endpoint: DataSourceEndpoint } diff --git a/apps/web/src/components/dashboard-builder/widgets/types/breakdown.tsx b/apps/web/src/components/dashboard-builder/widgets/types/breakdown.tsx index 6a8b2f275..3ead47a10 100644 --- a/apps/web/src/components/dashboard-builder/widgets/types/breakdown.tsx +++ b/apps/web/src/components/dashboard-builder/widgets/types/breakdown.tsx @@ -1,4 +1,5 @@ import { WIDGET_TYPES } from "@maple/domain/http" +import { makeQueryDataSource } from "@maple/widgets/dashboard" import { ArrowTrendDownIcon, @@ -51,19 +52,17 @@ const breakdownDataSource = ( // they receive — handing them 50 turns a 10-stage funnel into a truncated list. options?: { defaultLimit?: number }, ) => - ({ - endpoint: "custom_query_builder_breakdown", + makeQueryDataSource({ + resultShape: "breakdown", // Deliberately NOT forwarding `state.formulas`. A formula is a timeseries // expression with no meaning in a categorical breakdown, and // `QueryBuilderBreakdownInputSchema` (api/warehouse/query-builder-breakdown.ts) - // accepts only startTime/endTime/queries — smuggling formulas through the - // params to preserve them across a reopen fails the request decode and - // leaves the widget stuck on its loading skeleton. - params: { - queries: visibleQueries, - ...(options?.defaultLimit ? { defaultLimit: options.defaultLimit } : {}), - }, - transform: sharedTransform, + // accepts only startTime/endTime/queries — smuggling formulas through to + // preserve them across a reopen fails the request decode and leaves the + // widget stuck on its loading skeleton. + queries: visibleQueries, + ...(options?.defaultLimit ? { defaultLimit: options.defaultLimit } : {}), + ...(sharedTransform === undefined ? {} : { transform: sharedTransform }), }) satisfies WidgetDataSource export const pieWidgetType: WidgetTypeDefinition = { @@ -164,15 +163,13 @@ export const histogramWidgetType: WidgetTypeDefinition = { if (ctx.visibleQueries.some(hasActiveGroupBy)) return breakdownDataSource(ctx) const valueColumn = histogramValueColumn(ctx.visibleQueries[0]?.dataSource ?? "traces") - return { - endpoint: "custom_query_builder_list", - params: { - queries: ctx.visibleQueries, - limit: parsePositiveNumber(ctx.state.tableLimit) ?? 200, - ...(valueColumn ? { columns: [valueColumn] } : {}), - }, - transform: ctx.sharedTransform, - } + return makeQueryDataSource({ + resultShape: "list", + queries: ctx.visibleQueries, + limit: parsePositiveNumber(ctx.state.tableLimit) ?? 200, + ...(valueColumn ? { columns: [valueColumn] } : {}), + ...(ctx.sharedTransform === undefined ? {} : { transform: ctx.sharedTransform }), + }) }, buildDisplay: ({ base }) => base, diff --git a/apps/web/src/components/dashboard-builder/widgets/types/gauge.tsx b/apps/web/src/components/dashboard-builder/widgets/types/gauge.tsx index 8319ba0de..2bde9ba29 100644 --- a/apps/web/src/components/dashboard-builder/widgets/types/gauge.tsx +++ b/apps/web/src/components/dashboard-builder/widgets/types/gauge.tsx @@ -1,4 +1,5 @@ import { WIDGET_TYPES } from "@maple/domain/http" +import { dataSourceTransform } from "@maple/widgets/dashboard" import { SlidersIcon } from "@/components/icons" import { WidgetSettings } from "@/components/dashboard-builder/config/settings-fields" @@ -28,8 +29,8 @@ export const gaugeWidgetType: WidgetTypeDefinition = { presets: [], initialState: (widget) => ({ - statAggregate: toStatAggregate(widget.dataSource.transform?.reduceToValue?.aggregate), - statValueField: widget.dataSource.transform?.reduceToValue?.field ?? "", + statAggregate: toStatAggregate(dataSourceTransform(widget.dataSource)?.reduceToValue?.aggregate), + statValueField: dataSourceTransform(widget.dataSource)?.reduceToValue?.field ?? "", gaugeMin: widget.display.gauge?.min != null ? String(widget.display.gauge.min) : "", gaugeMax: widget.display.gauge?.max != null ? String(widget.display.gauge.max) : "", }), diff --git a/apps/web/src/components/dashboard-builder/widgets/types/list.tsx b/apps/web/src/components/dashboard-builder/widgets/types/list.tsx index 0c0ccdc82..f9032a2ac 100644 --- a/apps/web/src/components/dashboard-builder/widgets/types/list.tsx +++ b/apps/web/src/components/dashboard-builder/widgets/types/list.tsx @@ -1,4 +1,5 @@ import { DEFAULT_LIST_LIMIT, WIDGET_TYPES } from "@maple/domain/http" +import { makeQueryDataSource, makeRouteDataSource } from "@maple/widgets/dashboard" import { MenuIcon } from "@/components/icons" import { @@ -67,10 +68,10 @@ export const listWidgetType: WidgetTypeDefinition = { // Logs without rich filtering fall back to the simple list_logs endpoint. if (state.listDataSource === "logs") { - return { - endpoint: "list_logs", - params: buildListEndpointParams(state.listDataSource, state.listWhereClause, limit), - } + return makeRouteDataSource( + "list_logs", + buildListEndpointParams(state.listDataSource, state.listWhereClause, limit), + ) } // Traces go through the query engine, which supports full attr.* filtering. @@ -90,14 +91,12 @@ export const listWidgetType: WidgetTypeDefinition = { } const columnFields = state.listColumns.flatMap((column) => (column.field ? [column.field] : [])) - return { - endpoint: "custom_query_builder_list", - params: { - queries: [queryForEngine], - limit, - columns: columnFields.length > 0 ? columnFields : undefined, - }, - } + return makeQueryDataSource({ + resultShape: "list", + queries: [queryForEngine], + limit, + ...(columnFields.length > 0 ? { columns: columnFields } : {}), + }) }, // Built from scratch, not from `base`: a list has no chart presentation, no diff --git a/apps/web/src/components/dashboard-builder/widgets/types/markdown.tsx b/apps/web/src/components/dashboard-builder/widgets/types/markdown.tsx index 15f7a2049..011fb87fc 100644 --- a/apps/web/src/components/dashboard-builder/widgets/types/markdown.tsx +++ b/apps/web/src/components/dashboard-builder/widgets/types/markdown.tsx @@ -9,6 +9,7 @@ import { } from "@/components/dashboard-builder/widgets/widget-type-registry" import { PreviewFrame } from "@/components/dashboard-builder/widgets/types/preset-preview" import type { WidgetPresetDefinition } from "@/components/dashboard-builder/widgets/widget-definitions" +import { makeRouteDataSource } from "@maple/widgets/dashboard" /** The note's first few lines, with markdown syntax stripped. */ function MarkdownPresetPreview({ preset }: { preset: WidgetPresetDefinition }) { @@ -45,7 +46,7 @@ export const markdownWidgetType: WidgetTypeDefinition = { initialState: (widget) => ({ markdownContent: widget.display.markdown?.content ?? "" }), - buildDataSource: () => ({ endpoint: "markdown_static" }), + buildDataSource: () => makeRouteDataSource("markdown_static"), buildDisplay: ({ base, state }) => extendDisplay(base, { markdown: { content: state.markdownContent } }), } diff --git a/apps/web/src/components/dashboard-builder/widgets/types/stat.tsx b/apps/web/src/components/dashboard-builder/widgets/types/stat.tsx index 71da7af61..4e20c698d 100644 --- a/apps/web/src/components/dashboard-builder/widgets/types/stat.tsx +++ b/apps/web/src/components/dashboard-builder/widgets/types/stat.tsx @@ -1,4 +1,5 @@ import { WIDGET_TYPES } from "@maple/domain/http" +import { dataSourceTransform } from "@maple/widgets/dashboard" import { CirclePercentageIcon } from "@/components/icons" import { WidgetSettings } from "@/components/dashboard-builder/config/settings-fields" @@ -53,8 +54,8 @@ export const statWidgetType: WidgetTypeDefinition = { PresetPreview: StatPresetPreview, initialState: (widget) => ({ - statAggregate: toStatAggregate(widget.dataSource.transform?.reduceToValue?.aggregate), - statValueField: widget.dataSource.transform?.reduceToValue?.field ?? "", + statAggregate: toStatAggregate(dataSourceTransform(widget.dataSource)?.reduceToValue?.aggregate), + statValueField: dataSourceTransform(widget.dataSource)?.reduceToValue?.field ?? "", sparklineEnabled: widget.display.sparkline?.enabled === true, }), diff --git a/apps/web/src/components/dashboard-builder/widgets/types/table.tsx b/apps/web/src/components/dashboard-builder/widgets/types/table.tsx index dfe95e22e..b848d09d0 100644 --- a/apps/web/src/components/dashboard-builder/widgets/types/table.tsx +++ b/apps/web/src/components/dashboard-builder/widgets/types/table.tsx @@ -1,4 +1,5 @@ import { WIDGET_TYPES } from "@maple/domain/http" +import { makeQueryDataSource, dataSourceTransform } from "@maple/widgets/dashboard" import { GridIcon } from "@/components/icons" import { WidgetSettings } from "@/components/dashboard-builder/config/settings-fields" @@ -51,21 +52,20 @@ export const tableWidgetType: WidgetTypeDefinition = { }), initialState: (widget) => ({ - tableLimit: - typeof widget.dataSource.transform?.limit === "number" - ? String(widget.dataSource.transform.limit) - : "", + tableLimit: ((limit) => (typeof limit === "number" ? String(limit) : ""))( + dataSourceTransform(widget.dataSource)?.limit, + ), }), buildDataSource: ({ base, state, sharedTransform, visibleQueries }) => { const limit = parsePositiveNumber(state.tableLimit) if (visibleQueries.some(hasActiveGroupBy)) { - return { - endpoint: "custom_query_builder_breakdown", - params: { queries: visibleQueries }, - transform: limit ? { limit } : undefined, - } + return makeQueryDataSource({ + resultShape: "breakdown", + queries: visibleQueries, + ...(limit ? { transform: { limit } } : {}), + }) } if (!limit) return base diff --git a/apps/web/src/components/dashboard-builder/widgets/widget-actions-context.tsx b/apps/web/src/components/dashboard-builder/widgets/widget-actions-context.tsx index 484968b6d..0c2922a8a 100644 --- a/apps/web/src/components/dashboard-builder/widgets/widget-actions-context.tsx +++ b/apps/web/src/components/dashboard-builder/widgets/widget-actions-context.tsx @@ -9,6 +9,7 @@ import { type WidgetFixContext, } from "@/components/chat/widget-fix-context" import { encodeAlertChartToSearchParam } from "@/lib/alerts/widget-chart-param" +import { dataSourceRawSql, isQueryDataSource } from "@maple/widgets/dashboard" export interface WidgetActions { remove?: () => void @@ -99,13 +100,10 @@ export function WidgetActionsProvider({ const configure = readOnly ? undefined : () => configureWidget(widget.id) // "Create alert" is offered for query-driven charts; the alert builder - // warns when chart-only features need review. - const endpoint = widget.dataSource?.endpoint - const alertable = - endpoint === "raw_sql_chart" || - endpoint === "custom_query_builder_timeseries" || - endpoint === "custom_query_builder_breakdown" || - endpoint === "custom_query_builder_list" + // warns when chart-only features need review. Read structurally rather + // than by endpoint name so the action survives the v2 -> v3 data-source + // flip — an endpoint list would just make it disappear silently. + const alertable = isQueryDataSource(widget.dataSource) || dataSourceRawSql(widget.dataSource) !== null const createAlert = dashboardId && alertable ? () => { diff --git a/apps/web/src/components/dashboard-builder/widgets/widget-definitions.ts b/apps/web/src/components/dashboard-builder/widgets/widget-definitions.ts index c260f1dd5..89209199e 100644 --- a/apps/web/src/components/dashboard-builder/widgets/widget-definitions.ts +++ b/apps/web/src/components/dashboard-builder/widgets/widget-definitions.ts @@ -15,6 +15,7 @@ import type { WidgetDataSource, WidgetDisplayConfig, } from "@/components/dashboard-builder/types" +import { makeQueryDataSource, makeRouteDataSource } from "@maple/widgets/dashboard" export interface WidgetPresetDefinition { id: string @@ -33,12 +34,9 @@ export const statPresets: WidgetPresetDefinition[] = [ description: "Sum of traces across all services", icon: PulseIcon, visualization: "stat", - dataSource: { - endpoint: "service_usage", - transform: { - reduceToValue: { field: "totalTraces", aggregate: "sum" }, - }, - }, + dataSource: makeRouteDataSource("service_usage", undefined, { + reduceToValue: { field: "totalTraces", aggregate: "sum" }, + }), display: { title: "Total Traces", unit: "number", @@ -50,12 +48,9 @@ export const statPresets: WidgetPresetDefinition[] = [ description: "Sum of logs across all services", icon: FileIcon, visualization: "stat", - dataSource: { - endpoint: "service_usage", - transform: { - reduceToValue: { field: "totalLogs", aggregate: "sum" }, - }, - }, + dataSource: makeRouteDataSource("service_usage", undefined, { + reduceToValue: { field: "totalLogs", aggregate: "sum" }, + }), display: { title: "Total Logs", unit: "number", @@ -67,12 +62,9 @@ export const statPresets: WidgetPresetDefinition[] = [ description: "Overall error rate as percentage", icon: AlertWarningIcon, visualization: "stat", - dataSource: { - endpoint: "errors_summary", - transform: { - reduceToValue: { field: "errorRate", aggregate: "first" }, - }, - }, + dataSource: makeRouteDataSource("errors_summary", undefined, { + reduceToValue: { field: "errorRate", aggregate: "first" }, + }), display: { title: "Error Rate", unit: "percent", @@ -84,12 +76,9 @@ export const statPresets: WidgetPresetDefinition[] = [ description: "Total number of errors", icon: XmarkIcon, visualization: "stat", - dataSource: { - endpoint: "errors_summary", - transform: { - reduceToValue: { field: "totalErrors", aggregate: "first" }, - }, - }, + dataSource: makeRouteDataSource("errors_summary", undefined, { + reduceToValue: { field: "totalErrors", aggregate: "first" }, + }), display: { title: "Total Errors", unit: "number", @@ -101,13 +90,11 @@ export const statPresets: WidgetPresetDefinition[] = [ description: "Error rate for root spans only", icon: AlertWarningIcon, visualization: "stat", - dataSource: { - endpoint: "errors_summary", - params: { rootOnly: true }, - transform: { - reduceToValue: { field: "errorRate", aggregate: "first" }, - }, - }, + dataSource: makeRouteDataSource( + "errors_summary", + { rootOnly: true }, + { reduceToValue: { field: "errorRate", aggregate: "first" } }, + ), display: { title: "Root Error Rate", unit: "percent", @@ -119,13 +106,11 @@ export const statPresets: WidgetPresetDefinition[] = [ description: "Total number of errors on root spans", icon: XmarkIcon, visualization: "stat", - dataSource: { - endpoint: "errors_summary", - params: { rootOnly: true }, - transform: { - reduceToValue: { field: "totalErrors", aggregate: "first" }, - }, - }, + dataSource: makeRouteDataSource( + "errors_summary", + { rootOnly: true }, + { reduceToValue: { field: "totalErrors", aggregate: "first" } }, + ), display: { title: "Root Errors", unit: "number", @@ -137,12 +122,9 @@ export const statPresets: WidgetPresetDefinition[] = [ description: "Number of active services", icon: GridIcon, visualization: "stat", - dataSource: { - endpoint: "service_usage", - transform: { - reduceToValue: { field: "serviceName", aggregate: "count" }, - }, - }, + dataSource: makeRouteDataSource("service_usage", undefined, { + reduceToValue: { field: "serviceName", aggregate: "count" }, + }), display: { title: "Active Services", unit: "number", @@ -156,40 +138,34 @@ export const listPresets: WidgetPresetDefinition[] = [ name: "Recent Traces", description: "Latest traces with service, duration, and status", visualization: "list", - dataSource: { - endpoint: "custom_query_builder_list", - params: { - queries: [ - { - id: "preset-list-traces", - name: "A", - enabled: true, - dataSource: "traces", - signalSource: "default", - metricName: "", - metricType: "sum", - isMonotonic: false, - whereClause: "root_only = true", - aggregation: "count", - stepInterval: "", - orderByDirection: "desc", - addOns: { - groupBy: false, - having: false, - orderBy: false, - limit: false, - legend: false, - }, - groupBy: [], - having: "", - orderBy: "", - limit: "", - legend: "", + dataSource: makeQueryDataSource({ + resultShape: "list", + queries: [ + { + id: "preset-list-traces", + name: "A", + enabled: true, + dataSource: "traces", + whereClause: "root_only = true", + aggregation: "count", + stepInterval: "", + orderByDirection: "desc", + addOns: { + groupBy: false, + having: false, + orderBy: false, + limit: false, + legend: false, }, - ], - limit: 25, - }, - }, + groupBy: [], + having: "", + orderBy: "", + limit: "", + legend: "", + }, + ], + limit: 25, + }), display: { title: "Recent Traces", listDataSource: "traces", @@ -209,40 +185,34 @@ export const listPresets: WidgetPresetDefinition[] = [ name: "Error Traces", description: "Traces with errors", visualization: "list", - dataSource: { - endpoint: "custom_query_builder_list", - params: { - queries: [ - { - id: "preset-list-errors", - name: "A", - enabled: true, - dataSource: "traces", - signalSource: "default", - metricName: "", - metricType: "sum", - isMonotonic: false, - whereClause: "root_only = true AND has_error = true", - aggregation: "count", - stepInterval: "", - orderByDirection: "desc", - addOns: { - groupBy: false, - having: false, - orderBy: false, - limit: false, - legend: false, - }, - groupBy: [], - having: "", - orderBy: "", - limit: "", - legend: "", + dataSource: makeQueryDataSource({ + resultShape: "list", + queries: [ + { + id: "preset-list-errors", + name: "A", + enabled: true, + dataSource: "traces", + whereClause: "root_only = true AND has_error = true", + aggregation: "count", + stepInterval: "", + orderByDirection: "desc", + addOns: { + groupBy: false, + having: false, + orderBy: false, + limit: false, + legend: false, }, - ], - limit: 25, - }, - }, + groupBy: [], + having: "", + orderBy: "", + limit: "", + legend: "", + }, + ], + limit: 25, + }), display: { title: "Error Traces", listDataSource: "traces", @@ -262,10 +232,7 @@ export const listPresets: WidgetPresetDefinition[] = [ name: "Recent Logs", description: "Latest log entries", visualization: "list", - dataSource: { - endpoint: "list_logs", - params: { limit: 25 }, - }, + dataSource: makeRouteDataSource("list_logs", { limit: 25 }), display: { title: "Recent Logs", listDataSource: "logs", @@ -316,22 +283,19 @@ export const piePresets: WidgetPresetDefinition[] = [ description: "Distribution of errors across services", icon: AlertWarningIcon, visualization: "pie", - dataSource: { - endpoint: "custom_query_builder_breakdown", - params: { - queries: [ - buildBreakdownQuery(0, { - dataSource: "traces", - whereClause: "has_error = true", - aggregation: "count", - groupBy: ["service.name"], - }), - ], - formulas: [], - comparison: { mode: "none", includePercentChange: false }, - debug: false, - }, - }, + dataSource: makeQueryDataSource({ + resultShape: "breakdown", + queries: [ + buildBreakdownQuery(0, { + dataSource: "traces", + whereClause: "has_error = true", + aggregation: "count", + groupBy: ["service.name"], + }), + ], + formulas: [], + comparison: { mode: "none", includePercentChange: false }, + }), display: { title: "Errors by Service", chartId: "query-builder-pie", @@ -345,22 +309,19 @@ export const piePresets: WidgetPresetDefinition[] = [ description: "Distribution of log volume by severity level", icon: FileIcon, visualization: "pie", - dataSource: { - endpoint: "custom_query_builder_breakdown", - params: { - queries: [ - buildBreakdownQuery(0, { - dataSource: "logs", - whereClause: "", - aggregation: "count", - groupBy: ["severity"], - }), - ], - formulas: [], - comparison: { mode: "none", includePercentChange: false }, - debug: false, - }, - }, + dataSource: makeQueryDataSource({ + resultShape: "breakdown", + queries: [ + buildBreakdownQuery(0, { + dataSource: "logs", + whereClause: "", + aggregation: "count", + groupBy: ["severity"], + }), + ], + formulas: [], + comparison: { mode: "none", includePercentChange: false }, + }), display: { title: "Logs by Severity", chartId: "query-builder-pie", @@ -374,22 +335,19 @@ export const piePresets: WidgetPresetDefinition[] = [ description: "Distribution of trace volume across services", icon: PulseIcon, visualization: "pie", - dataSource: { - endpoint: "custom_query_builder_breakdown", - params: { - queries: [ - buildBreakdownQuery(0, { - dataSource: "traces", - whereClause: "root_only = true", - aggregation: "count", - groupBy: ["service.name"], - }), - ], - formulas: [], - comparison: { mode: "none", includePercentChange: false }, - debug: false, - }, - }, + dataSource: makeQueryDataSource({ + resultShape: "breakdown", + queries: [ + buildBreakdownQuery(0, { + dataSource: "traces", + whereClause: "root_only = true", + aggregation: "count", + groupBy: ["service.name"], + }), + ], + formulas: [], + comparison: { mode: "none", includePercentChange: false }, + }), display: { title: "Traces by Service", chartId: "query-builder-pie", @@ -406,22 +364,19 @@ export const funnelPresets: WidgetPresetDefinition[] = [ description: "Trace volume per service as a descending funnel", icon: PulseIcon, visualization: "funnel", - dataSource: { - endpoint: "custom_query_builder_breakdown", - params: { - queries: [ - buildBreakdownQuery(0, { - dataSource: "traces", - whereClause: "root_only = true", - aggregation: "count", - groupBy: ["service.name"], - }), - ], - formulas: [], - comparison: { mode: "none", includePercentChange: false }, - debug: false, - }, - }, + dataSource: makeQueryDataSource({ + resultShape: "breakdown", + queries: [ + buildBreakdownQuery(0, { + dataSource: "traces", + whereClause: "root_only = true", + aggregation: "count", + groupBy: ["service.name"], + }), + ], + formulas: [], + comparison: { mode: "none", includePercentChange: false }, + }), display: { title: "Traces by Service", chartId: "query-builder-funnel", @@ -435,22 +390,19 @@ export const funnelPresets: WidgetPresetDefinition[] = [ description: "Error volume per service ranked as a funnel", icon: AlertWarningIcon, visualization: "funnel", - dataSource: { - endpoint: "custom_query_builder_breakdown", - params: { - queries: [ - buildBreakdownQuery(0, { - dataSource: "traces", - whereClause: "has_error = true", - aggregation: "count", - groupBy: ["service.name"], - }), - ], - formulas: [], - comparison: { mode: "none", includePercentChange: false }, - debug: false, - }, - }, + dataSource: makeQueryDataSource({ + resultShape: "breakdown", + queries: [ + buildBreakdownQuery(0, { + dataSource: "traces", + whereClause: "has_error = true", + aggregation: "count", + groupBy: ["service.name"], + }), + ], + formulas: [], + comparison: { mode: "none", includePercentChange: false }, + }), display: { title: "Errors by Service", chartId: "query-builder-funnel", @@ -467,22 +419,19 @@ export const hbarPresets: WidgetPresetDefinition[] = [ description: "Top span names by volume, each as a share of the total", icon: ChartBarHorizontalIcon, visualization: "hbar", - dataSource: { - endpoint: "custom_query_builder_breakdown", - params: { - queries: [ - buildBreakdownQuery(0, { - dataSource: "traces", - whereClause: "", - aggregation: "count", - groupBy: ["span.name"], - }), - ], - formulas: [], - comparison: { mode: "none", includePercentChange: false }, - debug: false, - }, - }, + dataSource: makeQueryDataSource({ + resultShape: "breakdown", + queries: [ + buildBreakdownQuery(0, { + dataSource: "traces", + whereClause: "", + aggregation: "count", + groupBy: ["span.name"], + }), + ], + formulas: [], + comparison: { mode: "none", includePercentChange: false }, + }), display: { title: "Busiest Operations", chartId: "query-builder-hbar", @@ -495,22 +444,19 @@ export const hbarPresets: WidgetPresetDefinition[] = [ description: "Span volume per service, ranked", icon: PulseIcon, visualization: "hbar", - dataSource: { - endpoint: "custom_query_builder_breakdown", - params: { - queries: [ - buildBreakdownQuery(0, { - dataSource: "traces", - whereClause: "", - aggregation: "count", - groupBy: ["service.name"], - }), - ], - formulas: [], - comparison: { mode: "none", includePercentChange: false }, - debug: false, - }, - }, + dataSource: makeQueryDataSource({ + resultShape: "breakdown", + queries: [ + buildBreakdownQuery(0, { + dataSource: "traces", + whereClause: "", + aggregation: "count", + groupBy: ["service.name"], + }), + ], + formulas: [], + comparison: { mode: "none", includePercentChange: false }, + }), display: { title: "Spans by Service", chartId: "query-builder-hbar", @@ -529,41 +475,35 @@ export const histogramPresets: WidgetPresetDefinition[] = [ // A list of raw root-span durations, bucketized client-side by the // histogram chart — a count-by-service breakdown is NOT a duration // distribution (MAP-49). - dataSource: { - endpoint: "custom_query_builder_list", - params: { - queries: [ - { - id: "preset-histogram-durations", - name: "A", - enabled: true, - dataSource: "traces", - signalSource: "default", - metricName: "", - metricType: "sum", - isMonotonic: false, - whereClause: "root_only = true", - aggregation: "count", - stepInterval: "", - orderByDirection: "desc", - addOns: { - groupBy: false, - having: false, - orderBy: false, - limit: false, - legend: false, - }, - groupBy: [], - having: "", - orderBy: "", - limit: "", - legend: "", + dataSource: makeQueryDataSource({ + resultShape: "list", + queries: [ + { + id: "preset-histogram-durations", + name: "A", + enabled: true, + dataSource: "traces", + whereClause: "root_only = true", + aggregation: "count", + stepInterval: "", + orderByDirection: "desc", + addOns: { + groupBy: false, + having: false, + orderBy: false, + limit: false, + legend: false, }, - ], - limit: 200, - columns: ["durationMs"], - }, - }, + groupBy: [], + having: "", + orderBy: "", + limit: "", + legend: "", + }, + ], + limit: 200, + columns: ["durationMs"], + }), display: { title: "Trace Duration Distribution", chartId: "query-builder-histogram", @@ -577,22 +517,19 @@ export const histogramPresets: WidgetPresetDefinition[] = [ description: "Distribution of log volume across services", icon: ChartBarIcon, visualization: "histogram", - dataSource: { - endpoint: "custom_query_builder_breakdown", - params: { - queries: [ - buildBreakdownQuery(0, { - dataSource: "logs", - whereClause: "", - aggregation: "count", - groupBy: ["service.name"], - }), - ], - formulas: [], - comparison: { mode: "none", includePercentChange: false }, - debug: false, - }, - }, + dataSource: makeQueryDataSource({ + resultShape: "breakdown", + queries: [ + buildBreakdownQuery(0, { + dataSource: "logs", + whereClause: "", + aggregation: "count", + groupBy: ["service.name"], + }), + ], + formulas: [], + comparison: { mode: "none", includePercentChange: false }, + }), display: { title: "Log Volume by Service", chartId: "query-builder-histogram", @@ -609,32 +546,29 @@ export const heatmapPresets: WidgetPresetDefinition[] = [ description: "Density of errors across services and types", icon: ChartLineIcon, visualization: "heatmap", - dataSource: { - endpoint: "custom_query_builder_breakdown", - params: { - queries: [ - buildBreakdownQuery(0, { - name: "A", - legend: "Errors", - dataSource: "traces", - whereClause: "has_error = true", - aggregation: "count", - groupBy: ["service.name"], - }), - buildBreakdownQuery(1, { - name: "B", - legend: "OK", - dataSource: "traces", - whereClause: "has_error = false", - aggregation: "count", - groupBy: ["service.name"], - }), - ], - formulas: [], - comparison: { mode: "none", includePercentChange: false }, - debug: false, - }, - }, + dataSource: makeQueryDataSource({ + resultShape: "breakdown", + queries: [ + buildBreakdownQuery(0, { + name: "A", + legend: "Errors", + dataSource: "traces", + whereClause: "has_error = true", + aggregation: "count", + groupBy: ["service.name"], + }), + buildBreakdownQuery(1, { + name: "B", + legend: "OK", + dataSource: "traces", + whereClause: "has_error = false", + aggregation: "count", + groupBy: ["service.name"], + }), + ], + formulas: [], + comparison: { mode: "none", includePercentChange: false }, + }), display: { title: "Errors vs OK by Service", chartId: "query-builder-heatmap", @@ -651,7 +585,7 @@ export const markdownPresets: WidgetPresetDefinition[] = [ description: "Static markdown note for context, links, or runbooks", icon: FileIcon, visualization: "markdown", - dataSource: { endpoint: "markdown_static" }, + dataSource: makeRouteDataSource("markdown_static"), display: { title: "Note", markdown: { @@ -668,11 +602,7 @@ export const tablePresets: WidgetPresetDefinition[] = [ name: "Recent Traces", description: "Latest traces with duration and status", visualization: "table", - dataSource: { - endpoint: "list_traces", - params: { limit: 5 }, - transform: { limit: 5 }, - }, + dataSource: makeRouteDataSource("list_traces", { limit: 5 }, { limit: 5 }), display: { title: "Recent Traces", columns: [ @@ -687,11 +617,7 @@ export const tablePresets: WidgetPresetDefinition[] = [ name: "Errors by Type", description: "Error types with counts and affected services", visualization: "table", - dataSource: { - endpoint: "errors_by_type", - params: { limit: 5 }, - transform: { limit: 5 }, - }, + dataSource: makeRouteDataSource("errors_by_type", { limit: 5 }, { limit: 5 }), display: { title: "Errors by Type", columns: [ @@ -706,11 +632,7 @@ export const tablePresets: WidgetPresetDefinition[] = [ name: "Root Errors by Type", description: "Error types on root spans only", visualization: "table", - dataSource: { - endpoint: "errors_by_type", - params: { limit: 5, rootOnly: true }, - transform: { limit: 5 }, - }, + dataSource: makeRouteDataSource("errors_by_type", { limit: 5, rootOnly: true }, { limit: 5 }), display: { title: "Root Errors by Type", columns: [ @@ -725,9 +647,7 @@ export const tablePresets: WidgetPresetDefinition[] = [ name: "Service Overview", description: "Services with latency, errors, and throughput", visualization: "table", - dataSource: { - endpoint: "service_overview", - }, + dataSource: makeRouteDataSource("service_overview"), display: { title: "Service Overview", columns: [ diff --git a/apps/web/src/components/infra/planetscale/planetscale-alert-menu.tsx b/apps/web/src/components/infra/planetscale/planetscale-alert-menu.tsx index c33b7034a..a40e01816 100644 --- a/apps/web/src/components/infra/planetscale/planetscale-alert-menu.tsx +++ b/apps/web/src/components/infra/planetscale/planetscale-alert-menu.tsx @@ -14,6 +14,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@maple/ui/components/ui import { BellIcon } from "@/components/icons" import { encodeAlertChartToSearchParam } from "@/lib/alerts/widget-chart-param" import { planetScaleAlertSuggestions } from "./planetscale-alert-suggestions" +import { makeQueryDataSource } from "@maple/widgets/dashboard" /** * "Alert on this" for a PlanetScale database. @@ -71,10 +72,10 @@ export function PlanetScaleAlertMenu({ widget: { id: crypto.randomUUID(), visualization: "chart", - dataSource: { - endpoint: "custom_query_builder_timeseries", - params: { queries: [suggestion.draft] }, - }, + dataSource: makeQueryDataSource({ + resultShape: "timeseries", + queries: [suggestion.draft], + }), display: { title: `${suggestion.title} · ${database}` }, }, }) diff --git a/apps/web/src/components/metrics/metric-graduation-actions.tsx b/apps/web/src/components/metrics/metric-graduation-actions.tsx index 289481e20..d8def05a8 100644 --- a/apps/web/src/components/metrics/metric-graduation-actions.tsx +++ b/apps/web/src/components/metrics/metric-graduation-actions.tsx @@ -17,14 +17,15 @@ import { CopyButton } from "@maple/ui/components/ui/copy-button" import { encodeAlertChartToSearchParam } from "@/lib/alerts/widget-chart-param" import type { WidgetDataSource } from "@/components/dashboard-builder/types" import type { MetricsQueryDraft } from "@maple/query-engine/query-builder" +import { makeQueryDataSource } from "@maple/widgets/dashboard" function buildWidgetDataSource(draft: MetricsQueryDraft): WidgetDataSource { - return { - endpoint: "custom_query_builder_timeseries", + return makeQueryDataSource({ + resultShape: "timeseries", // A fresh query id: the explorer's stable atom-key id must not leak into // persisted widgets, where two adds would otherwise share one id. - params: { queries: [{ ...draft, id: crypto.randomUUID() }] }, - } + queries: [{ ...draft, id: crypto.randomUUID() }], + }) } interface MetricGraduationActionsProps { diff --git a/apps/web/src/components/widget-lab/widget-lab.tsx b/apps/web/src/components/widget-lab/widget-lab.tsx index a4ce7dd91..f24319dc4 100644 --- a/apps/web/src/components/widget-lab/widget-lab.tsx +++ b/apps/web/src/components/widget-lab/widget-lab.tsx @@ -47,6 +47,7 @@ import { type StatSparklineScenario, type ChartScenario, } from "@/components/widget-lab/scenarios" +import { makeQueryDataSource } from "@maple/widgets/dashboard" // Widget renderers take no action props — the card menu resolves them from // context. The lab renders outside a dashboard, so it supplies its own stubs. @@ -449,7 +450,7 @@ function StatScenarioCard({ scenario, mode }: { scenario: WidgetScenario; mode: * warehouse. It only has to be present, because `StatWidget` reaches the * sparkline branch on `display.sparkline.dataSource` being set. */ -const LAB_SPARKLINE_SOURCE = { endpoint: "custom_query_builder_timeseries", params: {} } as const +const LAB_SPARKLINE_SOURCE = makeQueryDataSource({ resultShape: "timeseries", queries: [] }) /** * The real `StatWidget`, with its sparkline series supplied instead of fetched. diff --git a/apps/web/src/hooks/use-metric-scoped-autocomplete.ts b/apps/web/src/hooks/use-metric-scoped-autocomplete.ts index 619d2b560..5bc20cb01 100644 --- a/apps/web/src/hooks/use-metric-scoped-autocomplete.ts +++ b/apps/web/src/hooks/use-metric-scoped-autocomplete.ts @@ -6,8 +6,9 @@ import { } from "@/lib/services/atoms/warehouse-query-atoms" import { disabledResultAtom } from "@/lib/services/atoms/disabled-result-atom" import type { WhereClauseAutocompleteValues } from "@/lib/query-builder/where-clause-autocomplete" +import type { QueryBuilderMetricType } from "@maple/query-model" -export type MetricScopedMetricType = "sum" | "gauge" | "histogram" | "exponential_histogram" +export type MetricScopedMetricType = QueryBuilderMetricType const METRIC_TYPES: ReadonlySet = new Set(["sum", "gauge", "histogram", "exponential_histogram"]) diff --git a/apps/web/src/hooks/use-widget-data.ts b/apps/web/src/hooks/use-widget-data.ts index 3f658d212..fcca9f649 100644 --- a/apps/web/src/hooks/use-widget-data.ts +++ b/apps/web/src/hooks/use-widget-data.ts @@ -1,4 +1,5 @@ import { useMemo, useState } from "react" +import { dataSourceTransform, type WidgetDataSourceTransformSchema } from "@maple/widgets/dashboard" import { Atom, Result } from "@/lib/effect-atom" import { useRefreshableAtomValue } from "@/hooks/use-refreshable-atom-value" import { Effect, Schedule, Schema } from "effect" @@ -82,7 +83,7 @@ function isSeriesNameHidden(seriesName: string, hiddenBaseNames: Set): b function filterHiddenSeriesRows( rows: Array>, - baseNames: string[], + baseNames: ReadonlyArray, ): Array> { if (baseNames.length === 0) return rows @@ -125,7 +126,10 @@ function interpolateParams( function applyTransform( // eslint-disable-next-line @typescript-eslint/no-explicit-any data: any, - transform: WidgetDataSource["transform"], + // The readonly schema type, not the app's deep-mutable `WidgetDataSource` + // alias: this only reads the transform, and `dataSourceTransform` hands back + // a live slice of the stored document that nothing here may write to. + transform: typeof WidgetDataSourceTransformSchema.Type | undefined, // eslint-disable-next-line @typescript-eslint/no-explicit-any ): any { if (!transform || !data) return data @@ -509,7 +513,7 @@ export function useWidgetDataSource( const result = useRefreshableAtomValue(fetchAtom) - const transform = dataSource?.transform + const transform = dataSourceTransform(dataSource) const dataState: WidgetDataState = useMemo(() => { if (isStatic) { diff --git a/apps/web/src/lib/alerts/widget-prefill.test.ts b/apps/web/src/lib/alerts/widget-prefill.test.ts index d65eb7469..c67e04181 100644 --- a/apps/web/src/lib/alerts/widget-prefill.test.ts +++ b/apps/web/src/lib/alerts/widget-prefill.test.ts @@ -45,6 +45,42 @@ describe("createWidgetAlertPrefill", () => { expect(result.notices).toEqual([]) }) + it("carries the chart's own reducer instead of defaulting to identity", () => { + const result = createWidgetAlertPrefill( + { + id: "w1", + dataSource: { + endpoint: "raw_sql_chart", + params: { sql: "SELECT max(Duration) AS value FROM traces WHERE $__orgFilter" }, + transform: { reduceToValue: { field: "value", aggregate: "max" } }, + }, + }, + defaultRuleForm(), + ) + + expect(result.form.rawQueryReducer).toBe("max") + expect(result.notices).toEqual([]) + }) + + it("warns rather than guessing when the chart reducer has no alert equivalent", () => { + const result = createWidgetAlertPrefill( + { + id: "w1", + dataSource: { + endpoint: "raw_sql_chart", + params: { sql: "SELECT count() AS value FROM traces WHERE $__orgFilter" }, + transform: { reduceToValue: { field: "value", aggregate: "count" } }, + }, + }, + defaultRuleForm(), + ) + + expect(result.form.rawQueryReducer).toBe("identity") + expect(result.notices.map((notice) => notice.message).join("\n")).toContain( + "alert rules cannot express", + ) + }) + it("warns when copied raw SQL does not clearly return value", () => { const result = createWidgetAlertPrefill( { diff --git a/apps/web/src/lib/alerts/widget-prefill.ts b/apps/web/src/lib/alerts/widget-prefill.ts index 0567f3e23..e9d629828 100644 --- a/apps/web/src/lib/alerts/widget-prefill.ts +++ b/apps/web/src/lib/alerts/widget-prefill.ts @@ -2,7 +2,8 @@ import type { QueryBuilderQueryDraftPayload } from "@maple/domain/http" import { normalizeRuleQueryDraft, rawSqlHasValueColumn, type RuleFormState } from "@/lib/alerts/form-utils" import { buildTimeseriesQuerySpec } from "@maple/query-engine/query-builder" -import { dataSourceQuerySet, dataSourceRawSql } from "@maple/widgets/dashboard" +import { SERIES_REDUCER_TO_ALERT_REDUCER, toQueryBuilderDataSource } from "@maple/query-model" +import { dataSourceQuerySet, dataSourceRawSql, dataSourceTransform } from "@maple/widgets/dashboard" export type WidgetAlertPrefillNotice = { severity: "warning" | "error" @@ -40,11 +41,7 @@ function widgetAlertName(widget: AlertableDashboardWidget): string { function isQueryDraftPayload(value: unknown): value is QueryBuilderQueryDraftPayload { const query = record(value) - const dataSource = query.dataSource - return ( - (dataSource === "traces" || dataSource === "logs" || dataSource === "metrics") && - typeof query.aggregation === "string" - ) + return toQueryBuilderDataSource(query.dataSource) !== null && typeof query.aggregation === "string" } function isEnabledVisibleQuery(query: QueryBuilderQueryDraftPayload): boolean { @@ -66,8 +63,7 @@ function hasHiddenSeries( queries: QueryBuilderQueryDraftPayload[], ): boolean { if (queries.some((query) => query.hidden === true)) return true - const transform = record(widget.dataSource?.transform) - const hideSeries = record(transform.hideSeries) + const hideSeries = record(dataSourceTransform(widget.dataSource)?.hideSeries) return Array.isArray(hideSeries.baseNames) && hideSeries.baseNames.length > 0 } @@ -130,12 +126,26 @@ export function createWidgetAlertPrefill( } } + // The chart's own reducer, not the blank form's `identity`. A stat tile + // showing max(latency) produced an alert that evaluated the last bucket, + // silently and with nothing on screen to say so. + const chartReducer = dataSourceTransform(widget.dataSource)?.reduceToValue?.aggregate + const rawQueryReducer = + chartReducer === undefined ? undefined : SERIES_REDUCER_TO_ALERT_REDUCER[chartReducer] + if (chartReducer !== undefined && rawQueryReducer === undefined) { + notices.push({ + severity: "warning", + message: `This chart reduces its series with "${chartReducer}", which alert rules cannot express; the alert evaluates the window's last value instead.`, + }) + } + return { form: { ...base, name: widgetAlertName(widget), signalType: "raw_query", rawQuerySql: sql, + ...(rawQueryReducer === undefined ? {} : { rawQueryReducer }), }, notices, } diff --git a/apps/web/src/lib/query-builder/widget-builder-shared.ts b/apps/web/src/lib/query-builder/widget-builder-shared.ts index 08d1d87a0..2b31f61fb 100644 --- a/apps/web/src/lib/query-builder/widget-builder-shared.ts +++ b/apps/web/src/lib/query-builder/widget-builder-shared.ts @@ -17,6 +17,7 @@ import type { } from "@/components/dashboard-builder/types" import type { LegendPosition } from "@/components/dashboard-builder/config/settings-fields" import { STAT_AGGREGATES, type StatAggregate } from "@maple/domain/http" +import type { QueryComparisonMode } from "@maple/query-model" import type { HeatmapColorScale, HeatmapScaleType } from "@maple/domain/http" import { normalizeKey, parseBoolean, parseWhereClause as parseWhereClauses } from "@maple/domain/where-clause" @@ -45,9 +46,8 @@ export interface QueryBuilderWidgetState { curveType: "linear" | "monotone" queries: QueryBuilderQueryDraft[] formulas: QueryBuilderFormulaDraft[] - comparisonMode: "none" | "previous_period" + comparisonMode: QueryComparisonMode includePercentChange: boolean - debug: boolean statAggregate: StatAggregate statValueField: string unit: ValueUnit diff --git a/apps/web/src/lib/query-builder/widget-builder-utils.test.ts b/apps/web/src/lib/query-builder/widget-builder-utils.test.ts index c45d6d3a9..eda3c1f0b 100644 --- a/apps/web/src/lib/query-builder/widget-builder-utils.test.ts +++ b/apps/web/src/lib/query-builder/widget-builder-utils.test.ts @@ -38,7 +38,6 @@ function makeState(): QueryBuilderWidgetState { formulas: [], comparisonMode: "none", includePercentChange: true, - debug: false, statAggregate: "first", statValueField: "", unit: "number", @@ -223,7 +222,7 @@ describe("funnel/heatmap endpoint routing (MAP-49)", () => { it("sends breakdown params the endpoint schema accepts, and nothing more", () => { // QueryBuilderBreakdownInputSchema accepts only startTime/endTime/queries - // and the optional defaultLimit. An extra key (formulas, comparison, debug) + // and the optional defaultLimit. An extra key (formulas, comparison) // fails the request decode and leaves the widget stuck on its loading // skeleton, so this is a contract test, not a style preference. const state = { diff --git a/apps/web/src/lib/query-builder/widget-builder-utils.ts b/apps/web/src/lib/query-builder/widget-builder-utils.ts index 4f5fbc938..9ac95d09a 100644 --- a/apps/web/src/lib/query-builder/widget-builder-utils.ts +++ b/apps/web/src/lib/query-builder/widget-builder-utils.ts @@ -20,7 +20,7 @@ import { type QueryBuilderWidgetState, } from "@/lib/query-builder/widget-builder-shared" import { WIDGET_TYPES } from "@maple/domain/http" -import { dataSourceQuerySet, dataSourceRouteParams } from "@maple/widgets/dashboard" +import { dataSourceQuerySet, dataSourceRouteParams, makeQueryDataSource } from "@maple/widgets/dashboard" // Lowering the widget editor's state to a persisted widget, and back. // @@ -93,7 +93,6 @@ export function toInitialState(widget: DashboardWidget): QueryBuilderWidgetState typeof rawComparison.includePercentChange === "boolean" ? rawComparison.includePercentChange : true, - debug: routeParams.debug === true, statAggregate: "first", statValueField: "", unit: @@ -152,19 +151,16 @@ function timeseriesDataSource(state: QueryBuilderWidgetState): { return { sharedTransform, - base: { - endpoint: "custom_query_builder_timeseries", - params: { - queries: state.queries, - formulas: state.formulas, - comparison: { - mode: state.comparisonMode, - includePercentChange: state.includePercentChange, - }, - debug: state.debug, + base: makeQueryDataSource({ + resultShape: "timeseries", + queries: state.queries, + formulas: state.formulas, + comparison: { + mode: state.comparisonMode, + includePercentChange: state.includePercentChange, }, - transform: sharedTransform, - }, + ...(sharedTransform === undefined ? {} : { transform: sharedTransform }), + }), } } diff --git a/apps/web/src/lib/query-builder/widget-type-cycle.test.ts b/apps/web/src/lib/query-builder/widget-type-cycle.test.ts index 4c4206f7c..bd424959e 100644 --- a/apps/web/src/lib/query-builder/widget-type-cycle.test.ts +++ b/apps/web/src/lib/query-builder/widget-type-cycle.test.ts @@ -59,7 +59,6 @@ function makeState(overrides: Partial = {}): QueryBuild formulas: [], comparisonMode: "none", includePercentChange: true, - debug: false, statAggregate: "first", statValueField: "", unit: "number", diff --git a/apps/web/src/routes/metrics/$metricName.tsx b/apps/web/src/routes/metrics/$metricName.tsx index c7d3a4ffe..d0e14bbcc 100644 --- a/apps/web/src/routes/metrics/$metricName.tsx +++ b/apps/web/src/routes/metrics/$metricName.tsx @@ -9,9 +9,10 @@ import { PageRefreshProvider } from "@/components/time-range-picker/page-refresh import { TimeRangeHeaderControls } from "@/components/time-range-picker/time-range-header-controls" import { AutocompleteValuesProvider } from "@/hooks/use-autocomplete-values" import { useEffectiveTimeRange } from "@/hooks/use-effective-time-range" +import { QUERY_BUILDER_METRIC_TYPES } from "@maple/query-model" const metricDetailSearchSchema = Schema.Struct({ - type: Schema.optional(Schema.Literals(["sum", "gauge", "histogram", "exponential_histogram"])), + type: Schema.optional(Schema.Literals(QUERY_BUILDER_METRIC_TYPES)), agg: Schema.optional(Schema.Literals(["avg", "sum", "min", "max", "count", "rate", "increase"])), where: Schema.optional(Schema.String), groupBy: Schema.optional(Schema.String), diff --git a/apps/web/src/routes/metrics/index.tsx b/apps/web/src/routes/metrics/index.tsx index d5c0c0f9e..4f41a6ff5 100644 --- a/apps/web/src/routes/metrics/index.tsx +++ b/apps/web/src/routes/metrics/index.tsx @@ -6,16 +6,19 @@ import { MetricsBrowse, type MetricsBrowsePatch } from "@/components/metrics/met import { TimeRangeSearchFields, applyTimeRangeSearch } from "@/components/time-range-picker/search" import { PageRefreshProvider } from "@/components/time-range-picker/page-refresh-context" import { TimeRangeHeaderControls } from "@/components/time-range-picker/time-range-header-controls" +import { + QUERY_BUILDER_METRIC_TYPES, + type QueryBuilderMetricType, + toQueryBuilderMetricType, +} from "@maple/query-model" -const METRIC_TYPE_VALUES = ["sum", "gauge", "histogram", "exponential_histogram"] as const - -function asMetricType(value: string): (typeof METRIC_TYPE_VALUES)[number] | undefined { - return METRIC_TYPE_VALUES.find((type) => type === value) +function asMetricType(value: string): QueryBuilderMetricType | undefined { + return toQueryBuilderMetricType(value) ?? undefined } const metricsSearchSchema = Schema.Struct({ q: Schema.optional(Schema.String), - type: Schema.optional(Schema.Literals(["sum", "gauge", "histogram", "exponential_histogram"])), + type: Schema.optional(Schema.Literals(QUERY_BUILDER_METRIC_TYPES)), view: Schema.optional(Schema.Literals(["grid", "table"])), ...TimeRangeSearchFields, }) diff --git a/packages/query-model/src/query-model.test.ts b/packages/query-model/src/query-model.test.ts index a52883f0b..f280732c9 100644 --- a/packages/query-model/src/query-model.test.ts +++ b/packages/query-model/src/query-model.test.ts @@ -5,6 +5,7 @@ import { ALERT_REDUCERS, QuerySetSchema, QueryBuilderQueryDraftSchema, + SERIES_REDUCER_TO_ALERT_REDUCER, SERIES_REDUCERS, TimeRangeSchema, } from "./index" @@ -133,6 +134,21 @@ describe("series reducers", () => { expect(SERIES_REDUCERS).toContain("count") expect(ALERT_REDUCERS).not.toContain("count" as never) }) + + it("round-trips every alert reducer through the series spelling and back", () => { + for (const reducer of ALERT_REDUCERS) { + expect(SERIES_REDUCER_TO_ALERT_REDUCER[ALERT_REDUCER_TO_SERIES_REDUCER[reducer]]).toBe(reducer) + } + }) + + it("leaves the widget-only reducer unmapped rather than defaulting it", () => { + // The whole point of the partial map: "create alert from chart" must be + // able to tell that a count tile has no alert equivalent, instead of + // silently sending `identity`. + expect(SERIES_REDUCER_TO_ALERT_REDUCER.count).toBeUndefined() + expect(SERIES_REDUCER_TO_ALERT_REDUCER.first).toBe("identity") + expect(SERIES_REDUCER_TO_ALERT_REDUCER.max).toBe("max") + }) }) describe("TimeRangeSchema", () => { diff --git a/packages/query-model/src/series-reducer.ts b/packages/query-model/src/series-reducer.ts index d7ce8cca3..0b0e0a971 100644 --- a/packages/query-model/src/series-reducer.ts +++ b/packages/query-model/src/series-reducer.ts @@ -42,3 +42,14 @@ export const ALERT_REDUCERS = REDUCER_TABLE.flatMap((entry) => export const ALERT_REDUCER_TO_SERIES_REDUCER = Object.fromEntries( REDUCER_TABLE.flatMap((entry) => (entry.alert === null ? [] : [[entry.alert, entry.series] as const])), ) as Record + +/** + * The other direction — PARTIAL, unlike the one above. + * + * `"count"` has no alert counterpart, so this returns `undefined` for it rather + * than a default. A chart reducer that silently became `identity` is the bug + * this exists to fix; a caller that gets `undefined` is expected to say so. + */ +export const SERIES_REDUCER_TO_ALERT_REDUCER = Object.fromEntries( + REDUCER_TABLE.flatMap((entry) => (entry.alert === null ? [] : [[entry.series, entry.alert] as const])), +) as Partial> diff --git a/packages/widgets/src/dashboard/access.ts b/packages/widgets/src/dashboard/access.ts index bc2b39470..e6ed13a2f 100644 --- a/packages/widgets/src/dashboard/access.ts +++ b/packages/widgets/src/dashboard/access.ts @@ -1,4 +1,5 @@ import type { QuerySet, QueryResultShape } from "@maple/query-model" +import type { WidgetDataSourceTransformV2 } from "./shared/transform" /** * Reading a widget's data source without caring which schema version wrote it. @@ -56,6 +57,28 @@ export const dataSourceEndpoint = (dataSource: unknown): string | null => { return typeof dataSource.endpoint === "string" ? dataSource.endpoint : null } +/** + * The client-side result reshaping (`reduceToValue`, `hideSeries`, `limit`, …). + * + * Version-independent by construction: `transform` describes what to do with the + * *response*, so it sits beside the data source's query on v2 and on every v3 + * arm alike. The accessor exists anyway because `construct.ts` writes this field + * and readers were reaching for it by hand — an asymmetry that would go + * unnoticed until the one version where it stops being true. + * + * NOT validated, like every accessor here: a stored transform with a reducer + * name the runtime dropped still reads back, and `applyTransform` falls through + * to its documented default rather than the tile failing to render. + */ +export const dataSourceTransform = ( + dataSource: unknown, +): typeof WidgetDataSourceTransformV2.Type | undefined => { + if (!isRecord(dataSource)) return undefined + return isRecord(dataSource.transform) + ? (dataSource.transform as typeof WidgetDataSourceTransformV2.Type) + : undefined +} + /** True when this data source carries a user-authored query set. */ export const isQueryDataSource = (dataSource: unknown): boolean => dataSourceQuerySet(dataSource) !== null @@ -68,37 +91,43 @@ export const isQueryDataSource = (dataSource: unknown): boolean => dataSourceQue * here: the MCP inspector and the template checker both want to report on what * is actually stored, not on what would survive a decode. */ -export const dataSourceQuerySet = ( - dataSource: unknown, -): (QuerySet & { resultShape: QueryResultShape }) | null => { +export interface WidgetQuerySet extends QuerySet { + readonly resultShape: QueryResultShape + /** Request shaping — see `QueryDataSourceInput` in `construct.ts`. */ + readonly defaultLimit?: number + readonly limit?: number + readonly columns?: ReadonlyArray +} + +export const dataSourceQuerySet = (dataSource: unknown): WidgetQuerySet | null => { if (!isRecord(dataSource)) return null - if (typeof dataSource.kind === "string") { - if (dataSource.kind !== "query") return null - const shape = dataSource.resultShape - return { - resultShape: typeof shape === "string" ? (shape as QueryResultShape) : "timeseries", - queries: Array.isArray(dataSource.queries) ? (dataSource.queries as QuerySet["queries"]) : [], - formulas: Array.isArray(dataSource.formulas) - ? (dataSource.formulas as QuerySet["formulas"]) - : undefined, - comparison: isRecord(dataSource.comparison) - ? (dataSource.comparison as QuerySet["comparison"]) - : undefined, + const source = (() => { + if (typeof dataSource.kind === "string") { + if (dataSource.kind !== "query") return null + const shape = dataSource.resultShape + return { + resultShape: typeof shape === "string" ? (shape as QueryResultShape) : "timeseries", + fields: dataSource, + } } - } - - const endpoint = dataSource.endpoint - if (typeof endpoint !== "string") return null - const resultShape = QUERY_ENDPOINT_SHAPES[endpoint] - if (resultShape === undefined) return null + const endpoint = dataSource.endpoint + if (typeof endpoint !== "string") return null + const resultShape = QUERY_ENDPOINT_SHAPES[endpoint] + if (resultShape === undefined) return null + return { resultShape, fields: isRecord(dataSource.params) ? dataSource.params : {} } + })() + if (source === null) return null - const params = isRecord(dataSource.params) ? dataSource.params : {} + const { resultShape, fields } = source return { resultShape, - queries: Array.isArray(params.queries) ? (params.queries as QuerySet["queries"]) : [], - formulas: Array.isArray(params.formulas) ? (params.formulas as QuerySet["formulas"]) : undefined, - comparison: isRecord(params.comparison) ? (params.comparison as QuerySet["comparison"]) : undefined, + queries: Array.isArray(fields.queries) ? (fields.queries as QuerySet["queries"]) : [], + formulas: Array.isArray(fields.formulas) ? (fields.formulas as QuerySet["formulas"]) : undefined, + comparison: isRecord(fields.comparison) ? (fields.comparison as QuerySet["comparison"]) : undefined, + defaultLimit: typeof fields.defaultLimit === "number" ? fields.defaultLimit : undefined, + limit: typeof fields.limit === "number" ? fields.limit : undefined, + columns: Array.isArray(fields.columns) ? (fields.columns as ReadonlyArray) : undefined, } } diff --git a/packages/widgets/src/dashboard/construct.ts b/packages/widgets/src/dashboard/construct.ts index 1c038003d..042694173 100644 --- a/packages/widgets/src/dashboard/construct.ts +++ b/packages/widgets/src/dashboard/construct.ts @@ -24,10 +24,35 @@ type WidgetDataSourceTransform = typeof WidgetDataSourceTransformV2.Type export interface QueryDataSourceInput extends QuerySet { readonly resultShape: QueryResultShape readonly transform?: WidgetDataSourceTransform + /** + * Per-shape request shaping: how many rows to fetch and which columns. + * + * NOT in `QuerySet` and NOT in `@maple/query-model`, deliberately. These + * describe the *request* a widget makes, not the query it stores — an alert + * rule shares the query and has no use for any of them. They live here + * because the alternative is what this file exists to remove: three widget + * types hand-assembling a params bag to smuggle one number through. + * + * `defaultLimit` is the breakdown's fetch-past-what-you-draw allowance (only + * the pie collapses a long tail into "Other"); `limit`/`columns` are the list + * shape's row cap and projection. + */ + readonly defaultLimit?: number + readonly limit?: number + readonly columns?: ReadonlyArray } -/** A widget backed by a user-authored query set. */ -export const makeQueryDataSource = (input: QueryDataSourceInput): WidgetDataSource => ({ +/** + * A widget backed by a user-authored query set. + * + * The endpoint stays a literal in the return type rather than widening to + * `string`: the web app narrows `WidgetDataSource["endpoint"]` to its registry's + * key union so `serverFunctionMap` is statically total, and a widened `string` + * would make every call site here unassignable to it. + */ +export const makeQueryDataSource = ( + input: QueryDataSourceInput & { readonly resultShape: S }, +): WidgetDataSource & { endpoint: (typeof QUERY_SHAPE_ENDPOINTS)[S] } => ({ endpoint: QUERY_SHAPE_ENDPOINTS[input.resultShape], params: { queries: input.queries, @@ -36,6 +61,9 @@ export const makeQueryDataSource = (input: QueryDataSourceInput): WidgetDataSour // widget that never had formulas indistinguishable from one that lost them. ...(input.formulas === undefined ? {} : { formulas: input.formulas }), ...(input.comparison === undefined ? {} : { comparison: input.comparison }), + ...(input.defaultLimit === undefined ? {} : { defaultLimit: input.defaultLimit }), + ...(input.limit === undefined ? {} : { limit: input.limit }), + ...(input.columns === undefined ? {} : { columns: input.columns }), }, ...(input.transform === undefined ? {} : { transform: input.transform }), }) @@ -45,7 +73,9 @@ export interface RawSqlDataSourceInput extends RawSqlDataSource { } /** A widget backed by user-authored ClickHouse SQL. */ -export const makeRawSqlDataSource = (input: RawSqlDataSourceInput): WidgetDataSource => ({ +export const makeRawSqlDataSource = ( + input: RawSqlDataSourceInput, +): WidgetDataSource & { endpoint: typeof RAW_SQL_ENDPOINT } => ({ endpoint: RAW_SQL_ENDPOINT, params: { sql: input.sql, @@ -63,11 +93,11 @@ export const makeRawSqlDataSource = (input: RawSqlDataSourceInput): WidgetDataSo * exists for symmetry and to give the sweep one shape to grep for, not because * the call site would otherwise break at the flip. */ -export const makeRouteDataSource = ( - endpoint: string, +export const makeRouteDataSource = ( + endpoint: E, params?: Record, transform?: WidgetDataSourceTransform, -): WidgetDataSource => ({ +): WidgetDataSource & { endpoint: E } => ({ endpoint, ...(params === undefined ? {} : { params }), ...(transform === undefined ? {} : { transform }), diff --git a/packages/widgets/src/dashboard/index.ts b/packages/widgets/src/dashboard/index.ts index c8a5e7c6f..cffcc05aa 100644 --- a/packages/widgets/src/dashboard/index.ts +++ b/packages/widgets/src/dashboard/index.ts @@ -24,10 +24,12 @@ export { dataSourceQuerySet, dataSourceRawSql, dataSourceRouteParams, + dataSourceTransform, isQueryDataSource, QUERY_SHAPE_ENDPOINTS, RAW_SQL_ENDPOINT, type RawSqlDataSource, + type WidgetQuerySet, } from "./access" export { makeQueryDataSource, From 7c083eac66519ca6eb21e967eee48698a967bfe0 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Thu, 13 Aug 2026 21:06:38 +0200 Subject: [PATCH 06/13] fix(web): declare the @maple/query-model workspace dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `apps/web` imports `@maple/query-model` from nine files but never declared it. This passed locally and failed in CI because the two installs differ: a full `bun install` hoists every workspace package to the root `node_modules`, so the import resolves whether or not it is declared. CI's web lanes use `install-filters: "@maple/web"` (ci.yml), and a filtered install only links a package's DECLARED dependencies — so `apps/web/node_modules/@maple/query-model` did not exist and typecheck, build and test all failed on TS2307. Verified by running CI's own commands with the cache bypassed: `turbo typecheck|build|test --filter=@maple/web --force` — 3/3 tasks each, 89 test files / 781 tests pass. --- apps/web/package.json | 1 + bun.lock | 1 + 2 files changed, 2 insertions(+) diff --git a/apps/web/package.json b/apps/web/package.json index dcb0385dc..0194e36b2 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -32,6 +32,7 @@ "@maple/effect-db": "workspace:*", "@maple/infra": "workspace:*", "@maple/query-engine": "workspace:*", + "@maple/query-model": "workspace:*", "@maple/thinking-orbs": "workspace:*", "@maple/ui": "workspace:*", "@maple/unitflow": "workspace:*", diff --git a/bun.lock b/bun.lock index ca79f21d9..30ea06b23 100644 --- a/bun.lock +++ b/bun.lock @@ -269,6 +269,7 @@ "@maple/effect-db": "workspace:*", "@maple/infra": "workspace:*", "@maple/query-engine": "workspace:*", + "@maple/query-model": "workspace:*", "@maple/thinking-orbs": "workspace:*", "@maple/ui": "workspace:*", "@maple/unitflow": "workspace:*", From 6159fada7b1a57ff653e34169c6e7f06ff4cddc4 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Thu, 13 Aug 2026 22:00:44 +0200 Subject: [PATCH 07/13] d --- .../rules/service-map-attribution.md | 6 + .../integrations/ScrapeTargetsService.ts | 110 +++++++++++++----- 2 files changed, 86 insertions(+), 30 deletions(-) diff --git a/.agents/skills/maple-telemetry-conventions/rules/service-map-attribution.md b/.agents/skills/maple-telemetry-conventions/rules/service-map-attribution.md index 86fda95e5..a5ae6e3d9 100644 --- a/.agents/skills/maple-telemetry-conventions/rules/service-map-attribution.md +++ b/.agents/skills/maple-telemetry-conventions/rules/service-map-attribution.md @@ -94,6 +94,12 @@ Keep these names consistent across services to avoid edge fragmentation on the m | `cloudflare-logpush` | Cloudflare logpush source | | `clerk` | Clerk auth | | `autumn` | Autumn metering | +| `github` | GitHub REST API (VCS sync) | +| `planetscale-metrics` | PlanetScale per-branch `/metrics` data plane, via the scrape proxy | +| `scrape-target` | Any other user-configured Prometheus scrape target, via the scrape proxy | + +The last two are deliberately generic. Scrape targets are user-configured and unbounded, so +naming a peer per target would grow the service map a node at a time. Bucket by target type. Add new peers to this list as they're introduced — single source of truth. diff --git a/apps/api/src/services/integrations/ScrapeTargetsService.ts b/apps/api/src/services/integrations/ScrapeTargetsService.ts index 92cd37559..905c344b7 100644 --- a/apps/api/src/services/integrations/ScrapeTargetsService.ts +++ b/apps/api/src/services/integrations/ScrapeTargetsService.ts @@ -901,6 +901,85 @@ export class ScrapeTargetsService extends Context.Service, + // The drizzle column is a plain string, not the domain union — keep it + // that way rather than casting a DB read into the branded type. + targetType: string, + timeoutMs: number, + ) { + const parsed = Option.liftThrowable(() => new URL(scrapeUrl))() + // Annotated before the fetch so a failed or timed-out scrape still + // draws its service-map edge. + yield* Effect.annotateCurrentSpan({ + "peer.service": + targetType === "planetscale" ? "planetscale-metrics" : "scrape-target", + "http.request.method": "GET", + "maple.scrape.target_type": targetType, + ...(Option.isSome(parsed) + ? { "server.address": parsed.value.host, "url.path": parsed.value.pathname } + : {}), + }) + + // `safeFetch` is retained for its SSRF protection + per-hop redirect + // re-validation (the Effect HttpClient transport has neither). The manual + // AbortController/setTimeout is replaced by the interruption-aware signal + // from `Effect.tryPromise` plus `Effect.timeout`: on timeout the fiber is + // interrupted, which aborts the in-flight fetch via that signal. + const result = yield* Effect.tryPromise({ + try: async (signal) => { + const response = await safeFetch(scrapeUrl, { + method: "GET", + headers, + signal, + }) + return { + status: response.status, + body: await response.text(), + contentType: + response.headers.get("content-type") ?? + "text/plain; version=0.0.4; charset=utf-8", + retryAfterSeconds: parseRetryAfterSeconds( + response.headers.get("retry-after"), + ), + } satisfies ScrapeTargetProxyResponse + }, + catch: toPersistenceError, + }).pipe( + Effect.timeout(timeoutMs), + // A timeout surfaces as the same persistence error a fetch abort + // produced before, so callers see no new error type. + Effect.catchTag("TimeoutError", () => + Effect.fail(toPersistenceError(new Error("The operation was aborted"))), + ), + ) + + yield* Effect.annotateCurrentSpan({ + "http.response.status_code": result.status, + // Decoded character count, not wire bytes — hence the vendor + // namespace rather than `http.response.body.size`. + "maple.scrape.response_chars": result.body.length, + }) + return result + }, + ) + const scrapeForCollector = Effect.fn("ScrapeTargetsService.scrapeForCollector")(function* ( targetId: ScrapeTargetId, subTargetKey?: string, @@ -955,36 +1034,7 @@ export class ScrapeTargetsService extends Context.Service { - const response = await safeFetch(scrapeUrl, { - method: "GET", - headers, - signal, - }) - return { - status: response.status, - body: await response.text(), - contentType: - response.headers.get("content-type") ?? - "text/plain; version=0.0.4; charset=utf-8", - retryAfterSeconds: parseRetryAfterSeconds(response.headers.get("retry-after")), - } satisfies ScrapeTargetProxyResponse - }, - catch: toPersistenceError, - }).pipe( - Effect.timeout(timeoutMs), - // A timeout surfaces as the same persistence error a fetch abort - // produced before, so callers see no new error type. - Effect.catchTag("TimeoutError", () => - Effect.fail(toPersistenceError(new Error("The operation was aborted"))), - ), - ) + return yield* fetchUpstream(scrapeUrl, headers, row.value.targetType, timeoutMs) }) const recordScrapeResults = Effect.fn("ScrapeTargetsService.recordScrapeResults")(function* ( From b53c560eec2b0baafd343aa0f5af4115908316b3 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Thu, 13 Aug 2026 23:04:00 +0200 Subject: [PATCH 08/13] refactor(query-engine): name the bucket-sizing policies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three surfaces auto-size time buckets and each passed its own literals at the call site: charts 100 points, alert evaluation 30, raw-SQL `$__interval_s` 30 with a 300s floor. Nothing said the numbers were deliberate, so `BUCKET_POLICIES` now names them and carries the reason each one differs — `alert` in particular re-tunes `minimumSampleCount` for every auto-sized rule if it moves. The string-parse-and-fall-back rule around `computeBucketSeconds` was open-coded twice, in the web app's `timeseries-utils` and as `computeAutoBucketSeconds` in the raw-SQL route, with identical behaviour and no shared home. Both now call `computeBucketSecondsForRange`. The web export stays as a thin alias because ~15 call sites import it from there. `computeRawSqlBuckets` still spelled out `Math.max(windowMinutes * 60, 60)` next to a call to `alertWindowBucketSeconds`, which is that same expression — the one site the previous pass missed. Same value, so no behaviour change; a raw-SQL rule whose `$__timeGroup` width drifted from its evaluation bucket would reduce over a different window than it was saved with. Also names the engine's ungrouped group key, which was a bare "all" at a dozen sites facing a `toStorageGroupKey` boundary that already named the other side. --- apps/api/src/routes/v1/query-engine.http.ts | 18 ++---- .../web/src/api/warehouse/timeseries-utils.ts | 26 ++++----- packages/query-engine/src/datetime.test.ts | 56 ++++++++++++++++++ packages/query-engine/src/datetime.ts | 58 +++++++++++++++++++ .../query-engine/src/runtime/query-engine.ts | 51 +++++++++++----- 5 files changed, 165 insertions(+), 44 deletions(-) diff --git a/apps/api/src/routes/v1/query-engine.http.ts b/apps/api/src/routes/v1/query-engine.http.ts index 0a8b155e7..80cd84661 100644 --- a/apps/api/src/routes/v1/query-engine.http.ts +++ b/apps/api/src/routes/v1/query-engine.http.ts @@ -78,7 +78,7 @@ import { WarehouseQueryService } from "@/services/warehouse/WarehouseQueryServic import { traceCacheTtlSeconds } from "@/services/warehouse/trace-detail-cache" import { CH, - computeBucketSeconds, + computeBucketSecondsForRange, formatWarehouseDateTime, parseWarehouseDateTime, QueryEngineExecuteBatchResponse, @@ -1793,17 +1793,11 @@ export const HttpQueryEngineLive = HttpApiBuilder.group(MapleApi, "queryEngine", * Auto-bucket for raw-SQL `$__interval_s` when the caller didn't supply * `granularitySeconds`. * - * Was a private ladder duplicating `computeBucketSeconds`; the only two - * differences were a 30-point target and a 300s floor, both of which the shared - * one now expresses. The floor is load-bearing: a sub-5-minute `$__interval_s` - * produces exactly the scan the granularity was chosen to avoid. + * Was a private ladder duplicating `computeBucketSeconds`, then a private copy of + * the string-parse-and-fall-back rule. Both now live in `BUCKET_POLICIES.rawSql`, + * whose 300s floor is load-bearing: a sub-5-minute `$__interval_s` produces + * exactly the scan the granularity was chosen to avoid. */ function computeAutoBucketSeconds(startTime: string, endTime: string): number { - const toEpochMs = (value: string) => new Date(value.replace(" ", "T") + "Z").getTime() - const startMs = toEpochMs(startTime) - const endMs = toEpochMs(endTime) - if (Number.isNaN(startMs) || Number.isNaN(endMs) || endMs <= startMs) { - return 300 - } - return computeBucketSeconds(startMs, endMs, { targetPoints: 30, minBucketSeconds: 300 }) + return computeBucketSecondsForRange(startTime, endTime, "rawSql") } diff --git a/apps/web/src/api/warehouse/timeseries-utils.ts b/apps/web/src/api/warehouse/timeseries-utils.ts index 0c951403e..7f24a55e3 100644 --- a/apps/web/src/api/warehouse/timeseries-utils.ts +++ b/apps/web/src/api/warehouse/timeseries-utils.ts @@ -1,6 +1,5 @@ -import { bucketTimeline, computeBucketSeconds as computeBucketSecondsMs } from "@maple/query-engine" +import { bucketTimeline, computeBucketSecondsForRange } from "@maple/query-engine" -const TARGET_POINTS = 100 const TINYBIRD_DATETIME_RE = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})(\.\d+)?$/ function toEpochMs(value: string): number { @@ -82,20 +81,15 @@ export function toIsoBucket(value: string | Date): string { return new Date(parsed).toISOString() } -export function computeBucketSeconds( - startTime?: string, - endTime?: string, - targetPoints = TARGET_POINTS, -): number { - if (!startTime || !endTime) return 300 - - const startMs = toEpochMs(startTime) - const endMs = toEpochMs(endTime) - if (Number.isNaN(startMs) || Number.isNaN(endMs) || endMs <= startMs) { - return 300 - } - - return computeBucketSecondsMs(startMs, endMs, { targetPoints }) +/** + * Chart-policy bucket width, from warehouse DateTime strings. + * + * A thin alias over the shared `computeBucketSecondsForRange` — kept as a local + * export because ~15 call sites import it from here, not because it does + * anything of its own. + */ +export function computeBucketSeconds(startTime?: string, endTime?: string, targetPoints?: number): number { + return computeBucketSecondsForRange(startTime, endTime, "chart", targetPoints) } /** diff --git a/packages/query-engine/src/datetime.test.ts b/packages/query-engine/src/datetime.test.ts index 7323c82a4..1134fe19c 100644 --- a/packages/query-engine/src/datetime.test.ts +++ b/packages/query-engine/src/datetime.test.ts @@ -1,9 +1,11 @@ import { describe, expect, it } from "vitest" import { bucketTimeline, + BUCKET_POLICIES, cacheSnapSecondsForRange, alertWindowBucketSeconds, computeBucketSeconds, + computeBucketSecondsForRange, formatWarehouseDateTime, formatWarehouseDateTimeMs, parseWarehouseDateTime, @@ -138,6 +140,60 @@ describe("computeBucketSeconds", () => { }) }) +describe("BUCKET_POLICIES", () => { + /** + * These three targets are NOT interchangeable, and a well-meaning "why do we + * have three of these?" cleanup is exactly what this guards. `alert` in + * particular re-tunes `minimumSampleCount` for every auto-sized rule if it + * moves. + */ + it("keeps the three surfaces on their own targets", () => { + expect(BUCKET_POLICIES.chart.targetPoints).toBe(100) + expect(BUCKET_POLICIES.alert.targetPoints).toBe(30) + expect(BUCKET_POLICIES.rawSql.targetPoints).toBe(30) + expect(BUCKET_POLICIES.rawSql.minBucketSeconds).toBe(300) + }) + + it("gives chart and alert measurably different granularity on the same window", () => { + const sixHours = 6 * 3600_000 + expect(computeBucketSeconds(0, sixHours, BUCKET_POLICIES.chart)).toBe(300) + expect(computeBucketSeconds(0, sixHours, BUCKET_POLICIES.alert)).toBe(900) + }) +}) + +describe("computeBucketSecondsForRange", () => { + it("parses warehouse DateTime strings under the chart policy", () => { + expect(computeBucketSecondsForRange("2026-02-01 00:00:00", "2026-02-01 00:30:00")).toBe(60) + expect(computeBucketSecondsForRange("2026-02-01 00:00:00", "2026-02-01 06:00:00")).toBe(300) + expect(computeBucketSecondsForRange("2026-02-01 00:00:00", "2026-02-08 00:00:00")).toBe(3600) + }) + + it("falls back rather than throwing on absent, unparseable or inverted ranges", () => { + expect(computeBucketSecondsForRange(undefined, "2026-02-01 00:00:00")).toBe(300) + expect(computeBucketSecondsForRange("2026-02-01 00:00:00", undefined)).toBe(300) + expect(computeBucketSecondsForRange("not a date", "2026-02-01 00:00:00")).toBe(300) + // endTime <= startTime is inverted, not a zero-width window. + expect(computeBucketSecondsForRange("2026-02-01 06:00:00", "2026-02-01 00:00:00")).toBe(300) + }) + + it("honors the rawSql floor through the policy name", () => { + expect(computeBucketSecondsForRange("2026-02-01 00:00:00", "2026-02-01 00:30:00", "rawSql")).toBe(300) + expect(computeBucketSecondsForRange("2026-02-01 00:00:00", "2026-02-01 06:00:00", "rawSql")).toBe(900) + }) + + it("lets a caller override the target for a denser histogram", () => { + expect(computeBucketSecondsForRange("2026-02-01 00:00:00", "2026-02-01 01:00:00", "chart", 60)).toBe( + 60, + ) + }) + + it("treats a tz-marked string the same as its tz-less spelling", () => { + expect(computeBucketSecondsForRange("2026-02-01T00:00:00Z", "2026-02-01T06:00:00Z")).toBe( + computeBucketSecondsForRange("2026-02-01 00:00:00", "2026-02-01 06:00:00"), + ) + }) +}) + describe("alertWindowBucketSeconds", () => { it("makes the evaluation window the bucket", () => { expect(alertWindowBucketSeconds(5)).toBe(300) diff --git a/packages/query-engine/src/datetime.ts b/packages/query-engine/src/datetime.ts index acd36c179..105ef7159 100644 --- a/packages/query-engine/src/datetime.ts +++ b/packages/query-engine/src/datetime.ts @@ -341,6 +341,64 @@ export function computeBucketSeconds( return bucket } +/** + * The bucket-sizing policies, one per surface that asks for an auto granularity. + * + * These numbers are NOT interchangeable and must not be collapsed into one + * default — that is the whole reason they are named here rather than passed as + * literals at each call site: + * + * - `chart` targets 100 points because a dashboard or explore chart is read by + * a human looking for spikes, and 30 points averages them away. + * - `alert` targets 30 because bucket width changes per-bucket values, and + * therefore changes `minimumSampleCount` behaviour, for every auto-sized + * rule. Making rules denser would silently re-tune every one of them. + * - `rawSql` backs `$__interval_s` and carries a 300s floor: a sub-5-minute + * bucket there produces exactly the scan the granularity was chosen to + * avoid. + * + * `fallbackSeconds` is what a caller gets for an unparseable or inverted range — + * see {@link computeBucketSecondsForRange}. + */ +export const BUCKET_POLICIES = { + chart: { targetPoints: 100, fallbackSeconds: 300 }, + alert: { targetPoints: 30, fallbackSeconds: 300 }, + rawSql: { targetPoints: 30, minBucketSeconds: 300, fallbackSeconds: 300 }, +} as const satisfies Record + +export type BucketPolicyName = keyof typeof BUCKET_POLICIES + +/** + * {@link computeBucketSeconds} for callers holding warehouse DateTime *strings* + * rather than epoch milliseconds, which is most of them. + * + * Exists because the string parse plus the "unparseable range falls back to a + * fixed width" rule were open-coded twice — once in the web app's + * `timeseries-utils`, once as `computeAutoBucketSeconds` in the raw-SQL route — + * with the same two behaviours and no shared home. `targetPoints` overrides the + * policy's own target for the few callers that want a denser histogram. + */ +export function computeBucketSecondsForRange( + startTime: string | undefined, + endTime: string | undefined, + policyName: BucketPolicyName = "chart", + targetPoints?: number, +): number { + const policy = BUCKET_POLICIES[policyName] + if (!startTime || !endTime) return policy.fallbackSeconds + + const startMs = parseWarehouseDateTime(startTime) + const endMs = parseWarehouseDateTime(endTime) + if (Number.isNaN(startMs) || Number.isNaN(endMs) || endMs <= startMs) { + return policy.fallbackSeconds + } + + return computeBucketSeconds(startMs, endMs, { + ...policy, + ...(targetPoints === undefined ? {} : { targetPoints }), + }) +} + /** * Bucket width for an alert rule's evaluation window. * diff --git a/packages/query-engine/src/runtime/query-engine.ts b/packages/query-engine/src/runtime/query-engine.ts index 964368062..b1ae290fa 100644 --- a/packages/query-engine/src/runtime/query-engine.ts +++ b/packages/query-engine/src/runtime/query-engine.ts @@ -31,6 +31,7 @@ import type { QueryProfileName, SqlQueryOptions, WarehouseQuerySettings } from " import { canonicalJSON } from "../canonical-json" import { alertWindowBucketSeconds, + BUCKET_POLICIES, computeBucketSeconds, formatWarehouseDateTime, parseWarehouseDateTime, @@ -215,6 +216,18 @@ export const msToTinybirdDateTime = (ms: number): string => { const CACHE_SNAP_S = 15 const TRACE_SERVICE_PARTITION_BUFFER_MS = 24 * 60 * 60 * 1000 +/** + * The engine's name for "this result has no grouping dimension". + * + * Storage, the wire and the UI spell the same thing `UNGROUPED_GROUP_KEY` + * (`"__total__"`), and `toStorageGroupKey` in the alerts service is the one + * boundary that translates between them. That translation was already named; + * this side was a bare `"all"` repeated at a dozen sites, which is what made it + * possible to write one of them as `"__total__"` by mistake and produce an + * `alert_rule_states` row no reader can see. + */ +export const ENGINE_UNGROUPED_GROUP_KEY = "all" + /** * Bound the service-enrichment lookup to the daily partitions surrounding the * rows already selected for this page. A one-day cushion covers traces that @@ -697,9 +710,9 @@ function groupAllMetricsTimeSeriesRows< const bucketOrder: string[] = fillOptions ? buildBucketTimeline(fillOptions.startMs, fillOptions.endMs, fillOptions.bucketSeconds) : [] - const isGrouped = rows.some((row) => row.groupName !== "all") + const isGrouped = rows.some((row) => row.groupName !== ENGINE_UNGROUPED_GROUP_KEY) const metricKey = (metric: string, groupName: string) => - isGrouped ? `${metric}::${groupName || "all"}` : metric + isGrouped ? `${metric}::${groupName || ENGINE_UNGROUPED_GROUP_KEY}` : metric for (const row of rows) { const bucket = normalizeBucket(row.bucket) @@ -738,7 +751,7 @@ function groupAllMetricsTimeSeriesRows< function collapseMetricTimeseriesRows( rows: ReadonlyArray, metric: Extract["metric"], -): Array<{ bucket: string; groupName: "all"; value: number }> { +): Array<{ bucket: string; groupName: typeof ENGINE_UNGROUPED_GROUP_KEY; value: number }> { const bucketMap = new Map< string, { @@ -771,7 +784,7 @@ function collapseMetricTimeseriesRows( .sort(([left], [right]) => left.localeCompare(right)) .map(([bucket, value]) => ({ bucket, - groupName: "all" as const, + groupName: ENGINE_UNGROUPED_GROUP_KEY, value: metric === "count" ? value.dataPointCount @@ -1140,7 +1153,7 @@ function shapeMetricsGroupRows< return groupTimeSeriesRows( rows.map((row) => ({ bucket: row.bucket, - groupName: "all" as const, + groupName: ENGINE_UNGROUPED_GROUP_KEY, value: valueExtractor(row), })), (r) => r.value, @@ -1992,7 +2005,7 @@ const composeMetricsGroupKey = ( serviceName: string, attributeValue: string, ): string => { - if (!groupBy || groupBy.length === 0 || groupBy.includes("none")) return "all" + if (!groupBy || groupBy.length === 0 || groupBy.includes("none")) return ENGINE_UNGROUPED_GROUP_KEY const parts: string[] = [] for (const dim of groupBy) { if (dim === "service") parts.push(serviceName || "") @@ -2001,7 +2014,7 @@ const composeMetricsGroupKey = ( else if (dim === "attribute" || dim === "resource_attribute") parts.push(attributeValue || "") } const filtered = parts.filter((p) => p.length > 0) - if (filtered.length === 0) return "all" + if (filtered.length === 0) return ENGINE_UNGROUPED_GROUP_KEY return filtered.join(" \u00b7 ") } @@ -2105,7 +2118,7 @@ export const computeAlertBuckets = Effect.fnUntraced(function* 0 ? tracesAggregateValueForMetric(query.metric, row) : null obs.push({ bucket: normalizeBucket(row.bucket), - groupKey: row.groupName || "all", + groupKey: row.groupName || ENGINE_UNGROUPED_GROUP_KEY, value, sampleCount, }) @@ -2124,7 +2137,7 @@ export const computeAlertBuckets = Effect.fnUntraced(function* 0 ? sampleCount : null, sampleCount, }) @@ -2179,7 +2192,11 @@ const computeRawSqlBuckets = Effect.fnUntraced(function* range: { readonly startTime: string; readonly endTime: string }, ) { const executeRawSql = makeExecuteRawSql(warehouse) - const granularitySeconds = Math.max(source.windowMinutes * 60, 60) + // The same rule `prepareAlertEvaluation` applies to a spec source, and it has + // to stay the same rule: a raw-SQL rule whose `$__timeGroup` width disagreed + // with its evaluation bucket would reduce over a different window than the one + // it was saved with. + const granularitySeconds = alertWindowBucketSeconds(source.windowMinutes) const { rows: rawRows } = yield* executeRawSql(tenant, { sql: source.sql, @@ -2214,7 +2231,8 @@ const computeRawSqlBuckets = Effect.fnUntraced(function* const seenGroups = new Set() for (const row of rows) { const rawGroup = row.group - const groupKey = typeof rawGroup === "string" && rawGroup.length > 0 ? rawGroup : "all" + const groupKey = + typeof rawGroup === "string" && rawGroup.length > 0 ? rawGroup : ENGINE_UNGROUPED_GROUP_KEY if (groupKey.length > MAX_RAW_SQL_GROUP_KEY_LENGTH) { return yield* new QueryEngineValidationError({ message: "Invalid raw SQL alert query", @@ -2277,7 +2295,7 @@ export const reduceAlertBuckets = ( else byGroup.set(o.groupKey, [entry]) } if (byGroup.size === 0) { - byGroup.set("all", [{ value: null, sampleCount: 0, hasData: false }]) + byGroup.set(ENGINE_UNGROUPED_GROUP_KEY, [{ value: null, sampleCount: 0, hasData: false }]) } return reducePerGroupObservations(byGroup, reducer) } @@ -2332,10 +2350,11 @@ const prepareAlertEvaluation = Effect.fnUntraced(function* (request: AlertEvalua } // Use the spec's bucketSeconds when present, otherwise auto-compute from the - // time range. Pinned to the historical 30-point target: the chart default is - // denser, but finer buckets would change per-bucket observation values (and - // `minimumSampleCount` behavior) for every rule that relies on auto sizing. - return query.bucketSeconds ?? computeBucketSeconds(startMs, endMs, { targetPoints: 30 }) + // time range. `BUCKET_POLICIES.alert` pins the historical 30-point target: the + // chart default is denser, but finer buckets would change per-bucket + // observation values (and `minimumSampleCount` behavior) for every rule that + // relies on auto sizing. + return query.bucketSeconds ?? computeBucketSeconds(startMs, endMs, BUCKET_POLICIES.alert) }) export const makeQueryEngineEvaluate = (warehouse: QueryEngineWarehouse) => From 3fafb35859c62ce2d002673f7ecae6b6df5290ee Mon Sep 17 00:00:00 2001 From: Makisuo Date: Thu, 13 Aug 2026 23:12:32 +0200 Subject: [PATCH 09/13] refactor(query-engine): extract the query-set shaping primitives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Folding N query drafts into chart-ready rows — merging by bucket, naming and zero-filling series, shifting a previous-period window, computing percent change, sizing execution windows — lived entirely inside one 856-line server function in apps/web. The MCP widget inspector needed the same thing and had grown its own copy; alerts are about to need it too. Moves the pure half into a new `@maple/query-engine/query-set` subpath, verbatim. Its own subpath rather than ./runtime or the root barrel for one concrete reason: apps/web/src/api/warehouse/* runs in the BROWSER, so everything here is browser-bundled — ./runtime pulls the ClickHouse DSL and is API-only, and the root barrel is imported for small helpers all over the place. The empty-range fallback comes along as an explicit, opt-in strategy value rather than an implicit default. It was already dead for dashboards — use-widget-data sends `enableEmptyRangeFallback: false` on every tile — and only the query-builder lab and metric detail page use it. Naming that keeps it away from alerts, where widening a window would breach on data outside the rule's own window. The tests come with the code: they only ever exercised pure helpers, so they now run against no HTTP at all, and the move made room for the cases that were missing — non-finite values, the ungrouped key's case-insensitivity, the name-collision suffix, and the order-dependence of the shared `usedSeriesNames` set that keeps ` (prev)` series from claiming the unsuffixed names. query-builder-timeseries.ts 856 -> 547, query-builder-breakdown.ts 195 -> 138. --- .../warehouse/query-builder-breakdown.test.ts | 47 +- .../api/warehouse/query-builder-breakdown.ts | 67 +-- .../query-builder-timeseries.test.ts | 397 ++++------------- .../api/warehouse/query-builder-timeseries.ts | 405 ++---------------- packages/query-engine/package.json | 1 + packages/query-engine/src/group-key.ts | 26 ++ packages/query-engine/src/index.ts | 1 + .../src/query-set/breakdown-merge.test.ts | 113 +++++ .../src/query-set/breakdown-merge.ts | 75 ++++ .../src/query-set/bucketing.test.ts | 187 ++++++++ .../query-engine/src/query-set/bucketing.ts | 163 +++++++ packages/query-engine/src/query-set/index.ts | 25 ++ .../src/query-set/series-merge.test.ts | 348 +++++++++++++++ .../src/query-set/series-merge.ts | 289 +++++++++++++ .../query-engine/src/runtime/query-engine.ts | 16 +- 15 files changed, 1382 insertions(+), 778 deletions(-) create mode 100644 packages/query-engine/src/group-key.ts create mode 100644 packages/query-engine/src/query-set/breakdown-merge.test.ts create mode 100644 packages/query-engine/src/query-set/breakdown-merge.ts create mode 100644 packages/query-engine/src/query-set/bucketing.test.ts create mode 100644 packages/query-engine/src/query-set/bucketing.ts create mode 100644 packages/query-engine/src/query-set/index.ts create mode 100644 packages/query-engine/src/query-set/series-merge.test.ts create mode 100644 packages/query-engine/src/query-set/series-merge.ts diff --git a/apps/web/src/api/warehouse/query-builder-breakdown.test.ts b/apps/web/src/api/warehouse/query-builder-breakdown.test.ts index 7c5b9cd5b..6f062fe86 100644 --- a/apps/web/src/api/warehouse/query-builder-breakdown.test.ts +++ b/apps/web/src/api/warehouse/query-builder-breakdown.test.ts @@ -1,8 +1,10 @@ import { describe, expect, it } from "vitest" import * as breakdownModule from "@/api/warehouse/query-builder-breakdown" -import { __testables, type QueryBuilderBreakdownInput } from "@/api/warehouse/query-builder-breakdown" +// `mergeBreakdownResults` moved to `@maple/query-engine/query-set`, where the +// legend-naming and ordering cases now live (`breakdown-merge.test.ts`). What is +// still this module's own responsibility is not rescaling on the way out. describe("query-builder breakdown units", () => { it("does not rescale error_rate values — the engine's 0–1 ratio is canonical", () => { // Regression guard: a ÷100 "normalize" survived from the Tinybird-pipe @@ -11,46 +13,3 @@ describe("query-builder breakdown units", () => { expect(breakdownModule.__testables).not.toHaveProperty("normalizeErrorRatePoints") }) }) - -describe("mergeBreakdownResults legend naming (MAP-49)", () => { - const queryDraft = (id: string, name: string, legend: string) => - ({ - id, - name, - legend, - enabled: true, - }) as unknown as QueryBuilderBreakdownInput["queries"][number] - - const result = (queryId: string, queryName: string, data: Array<{ name: string; value: number }>) => ({ - queryId, - queryName, - status: "success" as const, - error: null, - data, - }) - - it("uses query legends as merged column names, so heatmap axes read 'Errors'/'OK' instead of 'A'/'B'", () => { - const rows = __testables.mergeBreakdownResults( - [ - result("q-a", "A", [{ name: "demo-api", value: 12 }]), - result("q-b", "B", [ - { name: "demo-api", value: 480 }, - { name: "demo-worker", value: 210 }, - ]), - ], - [queryDraft("q-a", "A", "Errors"), queryDraft("q-b", "B", "OK")], - ) - - expect(rows).toContainEqual({ name: "demo-api", Errors: 12, OK: 480 }) - expect(rows).toContainEqual({ name: "demo-worker", Errors: 0, OK: 210 }) - }) - - it("falls back to the query name when no legend is set", () => { - const rows = __testables.mergeBreakdownResults( - [result("q-a", "A", [{ name: "x", value: 1 }]), result("q-b", "B", [{ name: "x", value: 2 }])], - [queryDraft("q-a", "A", ""), queryDraft("q-b", "B", "")], - ) - - expect(rows).toEqual([{ name: "x", A: 1, B: 2 }]) - }) -}) diff --git a/apps/web/src/api/warehouse/query-builder-breakdown.ts b/apps/web/src/api/warehouse/query-builder-breakdown.ts index bc5f1afaf..c780f7985 100644 --- a/apps/web/src/api/warehouse/query-builder-breakdown.ts +++ b/apps/web/src/api/warehouse/query-builder-breakdown.ts @@ -2,6 +2,7 @@ import { Effect, Result, Schema } from "effect" import { QueryBuilderQueryDraftSchema } from "@maple/domain/http" import { QueryEngineExecuteRequest } from "@maple/query-engine" import { buildBreakdownQuerySpec } from "@maple/query-engine/query-builder" +import { type BreakdownQueryResult, mergeBreakdownResults } from "@maple/query-engine/query-set" import { decodeInput, executeQueryEngine, invalidWarehouseInput } from "@/api/warehouse/effect-utils" import { displayError } from "@/lib/error-messages" @@ -22,14 +23,6 @@ const QueryBuilderBreakdownInputSchema = Schema.Struct({ export type QueryBuilderBreakdownInput = Schema.Schema.Type -interface BreakdownQueryResult { - queryId: string - queryName: string - status: "success" | "error" - error: string | null - data: Array<{ name: string; value: number }> -} - const executeBreakdownQuery = Effect.fn("QueryEngine.executeBreakdownQuery")(function* ( startTime: string, endTime: string, @@ -101,57 +94,6 @@ const executeBreakdownQuery = Effect.fn("QueryEngine.executeBreakdownQuery")(fun } satisfies BreakdownQueryResult }) -function toDisplayName(query: { name: string; legend?: string }): string { - const trimmedLegend = (query.legend ?? "").trim() - return trimmedLegend || query.name -} - -function mergeBreakdownResults( - results: BreakdownQueryResult[], - enabledQueries: QueryBuilderBreakdownInput["queries"], -): Array> { - const successful = results.filter((r) => r.status === "success" && r.data.length > 0) - if (successful.length === 0) return [] - - // Single query: return simple { name, value } rows - if (successful.length === 1) { - return successful[0].data - .map((item) => ({ name: item.name, value: item.value })) - .sort((a, b) => b.value - a.value) - } - - const rowsByName = new Map>() - const columnNames: string[] = [] - const queriesById = new Map(enabledQueries.map((q) => [q.id, q])) - - for (const result of successful) { - const query = queriesById.get(result.queryId) - const displayName = query ? toDisplayName(query) : result.queryName - columnNames.push(displayName) - - for (const item of result.data) { - const row = rowsByName.get(item.name) ?? { name: item.name } - row[displayName] = item.value - rowsByName.set(item.name, row) - } - } - - for (const row of rowsByName.values()) { - for (const col of columnNames) { - if (typeof row[col] !== "number") { - row[col] = 0 - } - } - } - - const firstCol = columnNames[0] - return Array.from(rowsByName.values()).toSorted((a, b) => { - const aVal = typeof a[firstCol] === "number" ? a[firstCol] : 0 - const bVal = typeof b[firstCol] === "number" ? b[firstCol] : 0 - return bVal - aVal - }) -} - export function getQueryBuilderBreakdown({ data }: { data: QueryBuilderBreakdownInput }) { return getQueryBuilderBreakdownEffect({ data }) } @@ -190,6 +132,7 @@ const getQueryBuilderBreakdownEffect = Effect.fn("QueryEngine.getQueryBuilderBre } }) -export const __testables = { - mergeBreakdownResults, -} +// The merge itself moved to `@maple/query-engine/query-set` and is tested there; +// what stays worth asserting here is that this module adds no rescaling of its +// own on the way out. +export const __testables = {} diff --git a/apps/web/src/api/warehouse/query-builder-timeseries.test.ts b/apps/web/src/api/warehouse/query-builder-timeseries.test.ts index 4b6dda3fd..ac5ab17f6 100644 --- a/apps/web/src/api/warehouse/query-builder-timeseries.test.ts +++ b/apps/web/src/api/warehouse/query-builder-timeseries.test.ts @@ -1,10 +1,17 @@ import { assert, describe, expect, it } from "@effect/vitest" import { Effect } from "effect" import type { QuerySpec } from "@maple/query-engine" +import { LAB_EMPTY_RANGE_STRATEGY, NO_EMPTY_RANGE_FALLBACK } from "@maple/query-engine/query-set" import { __testables } from "@/api/warehouse/query-builder-timeseries" import { WarehouseQueryError } from "@/api/warehouse/effect-utils" import type { QueryRunResult } from "@/components/query-builder/formula-results" +// The pure shaping this module used to own — bucket sizing, execution windows, +// the series merge, percent change, hidden-id collection — now lives in +// `@maple/query-engine/query-set` and is tested there against no HTTP at all. +// What is left here is this module's own: mapping the wire strategy shape, +// driving the executor through the fallback ladder, and the no-data message. + function makeQueryResult(overrides: Partial = {}): QueryRunResult { return { queryId: "q-1", @@ -18,129 +25,50 @@ function makeQueryResult(overrides: Partial = {}): QueryRunResul } } -describe("query-builder timeseries strategy", () => { - it("resolves deterministic auto bucket seconds for timeseries specs", () => { - const spec: QuerySpec = { - kind: "timeseries", - source: "traces", - metric: "count", - groupBy: ["service"], - } - - const resolved = __testables.resolveTimeseriesBucketSpec( - spec, - "2026-01-01 00:00:00", - "2026-01-02 00:00:00", - ) - - expect(resolved.kind).toBe("timeseries") - if (resolved.kind !== "timeseries") { - return - } - - expect(resolved.bucketSeconds).toBe(900) - }) - - it("does not mutate explicit bucket seconds", () => { - const spec: QuerySpec = { - kind: "timeseries", - source: "logs", - metric: "count", - bucketSeconds: 900, - } - - const resolved = __testables.resolveTimeseriesBucketSpec( - spec, - "2026-01-01 00:00:00", - "2026-01-01 03:00:00", - ) - - expect(resolved).toEqual(spec) - }) - - it("builds deterministic fallback execution windows", () => { - const windows = __testables.buildExecutionWindows( - "2026-01-02 00:00:00", - "2026-01-02 01:00:00", - { - enableEmptyRangeFallback: true, - fallbackWindowSeconds: [86400], - maxFallbackRangeSeconds: 86400 * 31, - }, - true, - ) - - expect(windows).toEqual([ - { - startTime: "2026-01-02 00:00:00", - endTime: "2026-01-02 01:00:00", - kind: "primary", - }, - { - startTime: "2026-01-01 01:00:00", - endTime: "2026-01-02 01:00:00", - kind: "fallback", - }, - ]) - }) - - it("resolves auto bucket per execution window (primary + fallback)", () => { - const spec: QuerySpec = { - kind: "timeseries", - source: "traces", - metric: "count", - } - - const primary = __testables.resolveExecutionSpecForWindow(spec, { - startTime: "2026-01-02 00:00:00", - endTime: "2026-01-02 01:00:00", - kind: "primary", - }) - const fallback = __testables.resolveExecutionSpecForWindow(spec, { - startTime: "2026-01-01 01:00:00", - endTime: "2026-01-02 01:00:00", - kind: "fallback", - }) - - expect(primary.kind).toBe("timeseries") - expect(fallback.kind).toBe("timeseries") - if (primary.kind !== "timeseries" || fallback.kind !== "timeseries") { - return - } - - expect(primary.bucketSeconds).toBe(60) - expect(fallback.bucketSeconds).toBe(900) - }) - - it("widens explicit bucket on fallback windows to stay within point budget", () => { - const spec: QuerySpec = { - kind: "timeseries", - source: "traces", - metric: "count", - bucketSeconds: 60, - } - - const primary = __testables.resolveExecutionSpecForWindow(spec, { - startTime: "2026-01-02 00:00:00", - endTime: "2026-01-02 01:00:00", - kind: "primary", - }) - const fallback = __testables.resolveExecutionSpecForWindow(spec, { - startTime: "2026-01-01 01:00:00", - endTime: "2026-01-02 01:00:00", - kind: "fallback", - }) - - expect(primary.kind).toBe("timeseries") - expect(fallback.kind).toBe("timeseries") - if (primary.kind !== "timeseries" || fallback.kind !== "timeseries") { - return - } - - expect(primary.bucketSeconds).toBe(60) - expect(fallback.bucketSeconds).toBe(900) +describe("resolveStrategy (wire shape → package shape)", () => { + it("maps the wire field names onto the strategy the package takes", () => { + expect( + __testables.resolveStrategy({ + startTime: "2026-01-01 00:00:00", + endTime: "2026-01-01 01:00:00", + queries: [], + strategy: { + enableEmptyRangeFallback: true, + fallbackWindowSeconds: [7200, 60], + maxFallbackRangeSeconds: 86400, + }, + }), + ).toEqual({ enabled: true, windowSeconds: [60, 7200], maxRangeSeconds: 86400 }) + }) + + /** + * `use-widget-data` sends `enableEmptyRangeFallback: false` on every dashboard + * tile. If that stopped disabling the ladder, an empty tile would silently + * start charting data from outside its own time range. + */ + it("honours the dashboard tile's explicit opt-out", () => { + expect( + __testables.resolveStrategy({ + startTime: "2026-01-01 00:00:00", + endTime: "2026-01-01 01:00:00", + queries: [], + strategy: { enableEmptyRangeFallback: false }, + }).enabled, + ).toBe(false) + }) + + it("defaults to the lab ladder when the caller sends no strategy", () => { + expect( + __testables.resolveStrategy({ + startTime: "2026-01-01 00:00:00", + endTime: "2026-01-01 01:00:00", + queries: [], + }), + ).toEqual(LAB_EMPTY_RANGE_STRATEGY) }) +}) +describe("executeTimeseriesQueryWithFallbackUsing", () => { it.effect("continues fallback execution after an error and recomputes window buckets", () => Effect.gen(function* () { const spec: QuerySpec = { @@ -155,9 +83,9 @@ describe("query-builder timeseries strategy", () => { "2026-01-02 01:00:00", spec, { - enableEmptyRangeFallback: true, - fallbackWindowSeconds: [24 * 60 * 60, 7 * 24 * 60 * 60], - maxFallbackRangeSeconds: 31 * 24 * 60 * 60, + enabled: true, + windowSeconds: [24 * 60 * 60, 7 * 24 * 60 * 60], + maxRangeSeconds: 31 * 24 * 60 * 60, }, true, (windowStart, _windowEnd, windowSpec) => @@ -201,27 +129,49 @@ describe("query-builder timeseries strategy", () => { }), ) - it("uses the shared auto bucket ladder", () => { - expect(__testables.computeBucketSeconds("2026-01-01 00:00:00", "2026-01-01 00:30:00")).toBe(60) - expect(__testables.computeBucketSeconds("2026-01-01 00:00:00", "2026-01-01 06:00:00")).toBe(300) - expect(__testables.computeBucketSeconds("2026-01-01 00:00:00", "2026-01-08 00:00:00")).toBe(3600) - }) + it.effect("fails outright when the PRIMARY window errors — that is not a widening case", () => + Effect.gen(function* () { + const outcome = yield* Effect.result( + __testables.executeTimeseriesQueryWithFallbackUsing( + "2026-01-02 00:00:00", + "2026-01-02 01:00:00", + { kind: "timeseries", source: "traces", metric: "count" }, + LAB_EMPTY_RANGE_STRATEGY, + true, + () => + Effect.gen(function* () { + return yield* new WarehouseQueryError({ operation: "test", message: "boom" }) + }), + ), + ) - it("counts only query results with real series data", () => { - const count = __testables.countSuccessfulQuerySeries([ - makeQueryResult({ - data: [{ bucket: "2026-01-01T00:00:00.000Z", series: {} }], - }), - makeQueryResult({ - queryId: "q-2", - queryName: "B", - data: [{ bucket: "2026-01-01T00:00:00.000Z", series: { total: 1 } }], - }), - ]) + assert.isTrue(outcome._tag === "Failure") + }), + ) - expect(count).toBe(1) - }) + it.effect("runs exactly one window when the strategy is off", () => + Effect.gen(function* () { + let calls = 0 + const result = yield* __testables.executeTimeseriesQueryWithFallbackUsing( + "2026-01-02 00:00:00", + "2026-01-02 01:00:00", + { kind: "timeseries", source: "traces", metric: "count" }, + NO_EMPTY_RANGE_FALLBACK, + true, + () => + Effect.sync(() => { + calls += 1 + return [] + }), + ) + assert.strictEqual(calls, 1) + assert.isFalse(result.fallbackUsed) + }), + ) +}) + +describe("noQueryDataMessage", () => { it("prefers query error message when no series data exists", () => { const message = __testables.noQueryDataMessage([ makeQueryResult({ @@ -237,168 +187,9 @@ describe("query-builder timeseries strategy", () => { expect(message).toContain("too expensive") }) +}) - it("preserves grouped series instead of summing them per query", () => { - const merged = __testables.mergeQueryRunResults( - [ - makeQueryResult({ - queryId: "q-1", - queryName: "A", - data: [ - { - bucket: "2026-01-01T00:00:00.000Z", - series: { checkout: 2, billing: 1 }, - }, - { - bucket: "2026-01-01T00:05:00.000Z", - series: { checkout: 4 }, - }, - ], - }), - makeQueryResult({ - queryId: "q-2", - queryName: "B", - data: [ - { - bucket: "2026-01-01T00:00:00.000Z", - series: { checkout: 5 }, - }, - { - bucket: "2026-01-01T00:05:00.000Z", - series: { checkout: 7 }, - }, - ], - }), - ], - new Map([ - ["q-1", "Errors"], - ["q-2", "Throughput"], - ]), - ) - - expect(merged.seriesNames).toEqual(["Errors: checkout", "Errors: billing", "Throughput: checkout"]) - - expect(merged.rowsByBucket.get("2026-01-01T00:00:00.000Z")).toEqual({ - bucket: "2026-01-01T00:00:00.000Z", - "Errors: checkout": 2, - "Errors: billing": 1, - "Throughput: checkout": 5, - }) - expect(merged.rowsByBucket.get("2026-01-01T00:05:00.000Z")).toEqual({ - bucket: "2026-01-01T00:05:00.000Z", - "Errors: checkout": 4, - "Errors: billing": 0, - "Throughput: checkout": 7, - }) - }) - - it("keeps non-grouped 'all' series as the display name", () => { - const merged = __testables.mergeQueryRunResults( - [ - makeQueryResult({ - queryId: "q-1", - queryName: "A", - data: [ - { - bucket: "2026-01-01T00:00:00.000Z", - series: { all: 12 }, - }, - ], - }), - ], - new Map([["q-1", "Requests"]]), - ) - - expect(merged.seriesNames).toEqual(["Requests"]) - expect(merged.rowsByBucket.get("2026-01-01T00:00:00.000Z")).toEqual({ - bucket: "2026-01-01T00:00:00.000Z", - Requests: 12, - }) - }) - - it("keeps formula series labels without redundant namespacing", () => { - const merged = __testables.mergeQueryRunResults( - [ - makeQueryResult({ - queryId: "f-1", - queryName: "F1", - source: "formula", - data: [ - { - bucket: "2026-01-01T00:00:00.000Z", - series: { "Error ratio": 0.3 }, - }, - ], - }), - ], - new Map([["f-1", "Error ratio"]]), - ) - - expect(merged.seriesNames).toEqual(["Error ratio"]) - expect(merged.rowsByBucket.get("2026-01-01T00:00:00.000Z")).toEqual({ - bucket: "2026-01-01T00:00:00.000Z", - "Error ratio": 0.3, - }) - }) - - it("computes percent change per stable grouped series", () => { - const rows: Array> = [ - { - bucket: "2026-01-01T00:00:00.000Z", - "Errors: checkout": 20, - "Errors: checkout (prev)": 10, - }, - ] - - __testables.appendPercentChangeSeries( - rows, - new Map([["q-1::checkout", "Errors: checkout"]]), - new Map([["q-1::checkout", "Errors: checkout (prev)"]]), - ) - - expect(rows[0]["Errors: checkout (%Δ)"]).toBe(100) - }) - - it("prev=0 & cur=0 is 0% (genuinely unchanged); prev=0 & cur>0 leaves a gap, not a fake 0%", () => { - const rows: Array> = [ - { - bucket: "2026-01-01T00:00:00.000Z", - "Errors: checkout": 0, - "Errors: checkout (prev)": 0, - }, - { - bucket: "2026-01-01T01:00:00.000Z", - "Errors: checkout": 5, - "Errors: checkout (prev)": 0, - }, - ] - - __testables.appendPercentChangeSeries( - rows, - new Map([["q-1::checkout", "Errors: checkout"]]), - new Map([["q-1::checkout", "Errors: checkout (prev)"]]), - ) - - expect(rows[0]["Errors: checkout (%Δ)"]).toBe(0) - expect(rows[1]).not.toHaveProperty("Errors: checkout (%Δ)") - }) - - // A ratio widget hides its numerator/denominator queries and plots only the formula. Merging - // the hidden operands in anyway put raw counts on the same axis as a 0–1 ratio — and, under - // the widget's own `percent` unit, drew them as "416849856400.0%". - it("collects hidden query and formula ids so they are dropped before merging", () => { - const hidden = __testables.collectHiddenResultIds({ - queries: [{ id: "num", hidden: true }, { id: "den", hidden: true }, { id: "plain" }], - formulas: [{ id: "ratio" }, { id: "scratch", hidden: true }], - }) - - expect([...hidden].sort()).toEqual(["den", "num", "scratch"]) - }) - - it("treats a widget with no formulas and nothing hidden as fully plotted", () => { - expect(__testables.collectHiddenResultIds({ queries: [{ id: "a" }, { id: "b" }] }).size).toBe(0) - }) - +describe("query-builder timeseries units", () => { it("does not rescale error_rate series — the engine's 0–1 ratio is canonical", () => { // Regression guard: a ÷100 "normalize" survived from the Tinybird-pipe // era (which returned percent points) long after the CH engine switched diff --git a/apps/web/src/api/warehouse/query-builder-timeseries.ts b/apps/web/src/api/warehouse/query-builder-timeseries.ts index de8691d03..4beddf9e7 100644 --- a/apps/web/src/api/warehouse/query-builder-timeseries.ts +++ b/apps/web/src/api/warehouse/query-builder-timeseries.ts @@ -19,6 +19,22 @@ import { QueryComparisonSchema, } from "@maple/query-model" import { buildTimeseriesQuerySpec } from "@maple/query-engine/query-builder" +import { + appendPercentChangeSeries, + buildExecutionWindows, + collectHiddenResultIds, + combineRows, + countSuccessfulQuerySeries, + type EmptyRangeFallbackStrategy, + hasAnySeriesData, + LAB_EMPTY_RANGE_STRATEGY, + mergeQueryRunResults, + resolveExecutionSpecForWindow, + resolveFallbackStrategy, + resolveTimeseriesBucketSpec, + shiftRunResults, + toDisplayNameById, +} from "@maple/query-engine/query-set" import { decodeInput, executeQueryEngine, @@ -27,18 +43,11 @@ import { type WarehouseApiError, } from "@/api/warehouse/effect-utils" import { displayError } from "@/lib/error-messages" -import { computeBucketSeconds } from "@/api/warehouse/timeseries-utils" type ExecuteError = WarehouseApiError | BackendError const dateTimeString = Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)) -const DEFAULT_STRATEGY = { - enableEmptyRangeFallback: true, - fallbackWindowSeconds: [24 * 60 * 60, 7 * 24 * 60 * 60, 31 * 24 * 60 * 60], - maxFallbackRangeSeconds: 31 * 24 * 60 * 60, -} as const - const StrategySchema = Schema.Struct({ enableEmptyRangeFallback: Schema.optional(Schema.Boolean), fallbackWindowSeconds: Schema.optional( @@ -105,46 +114,6 @@ interface QueryBuilderTimeseriesResponse { const toEpochMs = parseWarehouseDateTime -function resolveTimeseriesBucketSpec(spec: QuerySpec, startTime: string, endTime: string): QuerySpec { - if (spec.kind !== "timeseries" || spec.bucketSeconds) { - return spec - } - - return { - ...spec, - bucketSeconds: computeBucketSeconds(startTime, endTime), - } satisfies QuerySpec -} - -function resolveExecutionSpecForWindow( - spec: QuerySpec, - window: { startTime: string; endTime: string; kind: "primary" | "fallback" }, -): QuerySpec { - const resolved = resolveTimeseriesBucketSpec(spec, window.startTime, window.endTime) - if (resolved.kind !== "timeseries") { - return resolved - } - - if (window.kind !== "fallback") { - return resolved - } - - const autoBucketSeconds = computeBucketSeconds(window.startTime, window.endTime) - const selectedBucketSeconds = Math.max(resolved.bucketSeconds ?? autoBucketSeconds, autoBucketSeconds) - return { - ...resolved, - bucketSeconds: selectedBucketSeconds, - } -} - -function hasAnySeriesData(points: TimeseriesPoint[]): boolean { - return points.some((point) => Object.keys(point.series).length > 0) -} - -function countSuccessfulQuerySeries(results: QueryRunResult[]): number { - return results.filter((result) => result.status === "success" && hasAnySeriesData(result.data)).length -} - function noQueryDataMessage(queryResults: QueryRunResult[]): string { const firstQueryError = queryResults.find( (result) => typeof result.error === "string" && result.error.length > 0, @@ -153,87 +122,28 @@ function noQueryDataMessage(queryResults: QueryRunResult[]): string { return firstQueryError ?? NO_QUERY_DATA_MESSAGE } -function shiftBucket(bucket: string, offsetMs: number): string { - const parsed = new Date(bucket).getTime() - if (Number.isNaN(parsed)) { - return bucket - } - - return new Date(parsed + offsetMs).toISOString() -} - -function shiftResultPoints(points: TimeseriesPoint[], offsetMs: number): TimeseriesPoint[] { - return points.map((point) => ({ - bucket: shiftBucket(point.bucket, offsetMs), - series: { ...point.series }, - })) -} - -function resolveStrategy(input: QueryBuilderTimeseriesInput): { - enableEmptyRangeFallback: boolean - fallbackWindowSeconds: number[] - maxFallbackRangeSeconds: number -} { - const uniqueWindows = new Set( - (input.strategy?.fallbackWindowSeconds ?? DEFAULT_STRATEGY.fallbackWindowSeconds).filter( - (seconds) => Number.isFinite(seconds) && seconds > 0, - ), +/** + * The wire strategy shape (`enableEmptyRangeFallback` / `fallbackWindowSeconds` / + * `maxFallbackRangeSeconds`) mapped onto the package's. + * + * The wire names stay as they are: `use-widget-data` sends them on every widget + * fetch, so renaming them would be a behaviour change dressed as a refactor. + */ +function resolveStrategy(input: QueryBuilderTimeseriesInput): EmptyRangeFallbackStrategy { + return resolveFallbackStrategy( + { + ...(input.strategy?.enableEmptyRangeFallback === undefined + ? {} + : { enabled: input.strategy.enableEmptyRangeFallback }), + ...(input.strategy?.fallbackWindowSeconds === undefined + ? {} + : { windowSeconds: input.strategy.fallbackWindowSeconds }), + ...(input.strategy?.maxFallbackRangeSeconds === undefined + ? {} + : { maxRangeSeconds: input.strategy.maxFallbackRangeSeconds }), + }, + LAB_EMPTY_RANGE_STRATEGY, ) - - return { - enableEmptyRangeFallback: - input.strategy?.enableEmptyRangeFallback ?? DEFAULT_STRATEGY.enableEmptyRangeFallback, - fallbackWindowSeconds: Array.from(uniqueWindows).toSorted((left, right) => left - right), - maxFallbackRangeSeconds: - input.strategy?.maxFallbackRangeSeconds ?? DEFAULT_STRATEGY.maxFallbackRangeSeconds, - } -} - -function buildExecutionWindows( - startTime: string, - endTime: string, - strategy: ReturnType, - allowFallback: boolean, -): Array<{ startTime: string; endTime: string; kind: "primary" | "fallback" }> { - const startMs = toEpochMs(startTime) - const endMs = toEpochMs(endTime) - if (Number.isNaN(startMs) || Number.isNaN(endMs) || endMs <= startMs) { - return [{ startTime, endTime, kind: "primary" }] - } - - const rangeSeconds = Math.max((endMs - startMs) / 1000, 1) - const windows: Array<{ startTime: string; endTime: string; kind: "primary" | "fallback" }> = [ - { startTime, endTime, kind: "primary" }, - ] - - if (!allowFallback || !strategy.enableEmptyRangeFallback) { - return windows - } - - const seen = new Set([`${startTime}|${endTime}`]) - for (const seconds of strategy.fallbackWindowSeconds) { - if (seconds <= rangeSeconds || seconds > strategy.maxFallbackRangeSeconds) { - continue - } - - const windowStartMs = endMs - seconds * 1000 - const nextStart = formatWarehouseDateTime(windowStartMs) - const nextEnd = formatWarehouseDateTime(endMs) - const key = `${nextStart}|${nextEnd}` - - if (seen.has(key)) { - continue - } - - seen.add(key) - windows.push({ - startTime: nextStart, - endTime: nextEnd, - kind: "fallback", - }) - } - - return windows } const executeTimeseriesQuery = Effect.fn("QueryEngine.executeTimeseriesQuery")(function* ( @@ -348,199 +258,6 @@ const executeTimeseriesQueryWithFallbackUsing = Effect.fn("QueryEngine.executeTi }, ) -function toDisplayNameById( - entries: Array<{ id: string; name: string; legend?: string }>, -): Map { - const map = new Map() - - for (const entry of entries) { - const trimmedLegend = (entry.legend ?? "").trim() - map.set(entry.id, trimmedLegend || entry.name) - } - - return map -} - -function toSeriesDescriptor( - result: QueryRunResult, - displayName: string, - rawGroupName: string, - singleQuery: boolean, -): { - stableGroupKey: string - seriesLabel: string -} { - const normalizedGroupName = rawGroupName.trim() || "unnamed" - const isAllGroup = normalizedGroupName.toLowerCase() === "all" - const isFormulaSelfNamed = result.source === "formula" && normalizedGroupName === displayName - - if (isAllGroup || isFormulaSelfNamed) { - return { - stableGroupKey: "__all__", - seriesLabel: displayName, - } - } - - return { - stableGroupKey: normalizedGroupName, - seriesLabel: singleQuery ? normalizedGroupName : `${displayName}: ${normalizedGroupName}`, - } -} - -function mergeQueryRunResults( - results: QueryRunResult[], - displayNameById: Map, - options?: { - seriesSuffix?: string - usedSeriesNames?: Set - }, -): { - rowsByBucket: Map> - seriesNameByStableKey: Map - seriesNames: string[] -} { - const rowsByBucket = new Map>() - const usedSeriesNames = options?.usedSeriesNames ?? new Set() - const seriesNameByStableKey = new Map() - const seriesNames: string[] = [] - const suffix = options?.seriesSuffix ?? "" - - const uniqueName = (base: string): string => { - if (!usedSeriesNames.has(base)) { - usedSeriesNames.add(base) - return base - } - - let suffix = 2 - while (usedSeriesNames.has(`${base} (${suffix})`)) { - suffix += 1 - } - - const next = `${base} (${suffix})` - usedSeriesNames.add(next) - return next - } - - const successfulResultCount = results.filter( - (r) => r.status === "success" && r.data.length > 0 && hasAnySeriesData(r.data), - ).length - const singleQuery = successfulResultCount <= 1 - - for (const result of results) { - if (result.status !== "success") { - continue - } - - if (result.data.length === 0 || !hasAnySeriesData(result.data)) { - continue - } - - const preferredName = displayNameById.get(result.queryId) ?? result.queryName - - for (const point of result.data) { - const row = rowsByBucket.get(point.bucket) ?? { bucket: point.bucket } - if (Object.keys(point.series).length > 0) { - for (const [groupName, rawValue] of Object.entries(point.series)) { - const value = typeof rawValue === "number" ? rawValue : Number(rawValue) - if (!Number.isFinite(value)) { - continue - } - - const descriptor = toSeriesDescriptor(result, preferredName, groupName, singleQuery) - const stableKey = `${result.queryId}::${descriptor.stableGroupKey}` - let seriesName = seriesNameByStableKey.get(stableKey) - - if (!seriesName) { - seriesName = uniqueName(`${descriptor.seriesLabel}${suffix}`) - seriesNameByStableKey.set(stableKey, seriesName) - seriesNames.push(seriesName) - } - - row[seriesName] = value - } - } - rowsByBucket.set(point.bucket, row) - } - } - - for (const row of rowsByBucket.values()) { - for (const seriesName of seriesNames) { - if (typeof row[seriesName] !== "number") { - row[seriesName] = 0 - } - } - } - - return { - rowsByBucket, - seriesNameByStableKey, - seriesNames, - } -} - -function combineRows( - mergedSets: Array<{ - rowsByBucket: Map> - seriesNames: string[] - }>, -): Array> { - const rowsByBucket = new Map>() - const allSeriesNames = new Set() - - for (const merged of mergedSets) { - for (const seriesName of merged.seriesNames) { - allSeriesNames.add(seriesName) - } - - for (const [bucket, row] of merged.rowsByBucket.entries()) { - const existing = rowsByBucket.get(bucket) ?? { bucket } - rowsByBucket.set(bucket, { ...existing, ...row }) - } - } - - for (const row of rowsByBucket.values()) { - for (const seriesName of allSeriesNames) { - if (typeof row[seriesName] !== "number") { - row[seriesName] = 0 - } - } - } - - return Array.from(rowsByBucket.values()).toSorted((left, right) => - String(left.bucket).localeCompare(String(right.bucket)), - ) -} - -function appendPercentChangeSeries( - rows: Array>, - currentSeriesByStableKey: Map, - previousSeriesByStableKey: Map, -): void { - for (const [stableKey, currentSeriesName] of currentSeriesByStableKey.entries()) { - const previousSeriesName = previousSeriesByStableKey.get(stableKey) - if (!previousSeriesName) { - continue - } - - const deltaSeriesName = `${currentSeriesName} (%Δ)` - for (const row of rows) { - const current = row[currentSeriesName] - const previous = row[previousSeriesName] - - const currentValue = typeof current === "number" && Number.isFinite(current) ? current : 0 - const previousValue = typeof previous === "number" && Number.isFinite(previous) ? previous : 0 - - // prev=0 & cur=0 is genuinely "unchanged"; prev=0 & cur>0 has no - // meaningful percent — omit the point (gap) instead of fabricating 0%. - if (previousValue === 0) { - if (currentValue === 0) row[deltaSeriesName] = 0 - continue - } - row[deltaSeriesName] = ((currentValue - previousValue) / Math.abs(previousValue)) * 100 - } - } -} - const runQueryWindow = Effect.fn("QueryEngine.runQueryWindow")(function* ( startTime: string, endTime: string, @@ -655,45 +372,15 @@ const runQueryWindow = Effect.fn("QueryEngine.runQueryWindow")(function* ( } }) -/** - * Ids of queries and formulas whose series must not be plotted. - * - * `hidden` means "feed the formulas, don't draw me" — a hidden query still runs, because the - * formula that references it needs its numbers. The query-builder UI has always honored the flag - * (`widget-builder-utils`) but this data source did not, so a saved widget built on a hidden - * numerator/denominator plotted its raw operands next to the formula. On a ratio widget that also - * means raw counts rendered with the ratio's unit — the Cloudflare cache-hit chart drew - * "416849856400.0%" beside its real 0–1 hit rate. - */ -function collectHiddenResultIds(input: { - queries: ReadonlyArray<{ id: string; hidden?: boolean }> - formulas?: ReadonlyArray<{ id: string; hidden?: boolean }> -}): Set { - return new Set([ - ...input.queries.filter((query) => query.hidden).map((query) => query.id), - ...(input.formulas ?? []).filter((formula) => formula.hidden).map((formula) => formula.id), - ]) -} - -function shiftRunResults(results: QueryRunResult[], shiftMs: number): QueryRunResult[] { - return results.map((result) => ({ - ...result, - data: shiftResultPoints(result.data, shiftMs), - })) -} - +// The pure shaping — bucket sizing, execution windows, the series merge, percent +// change, hidden-id collection — moved to `@maple/query-engine/query-set` and is +// tested there. What remains here is this module's own: the wire strategy +// mapping, the fallback loop that drives the HTTP executor, and the "why is +// there no data" message. export const __testables = { - computeBucketSeconds, - resolveTimeseriesBucketSpec, - resolveExecutionSpecForWindow, - buildExecutionWindows, resolveStrategy, executeTimeseriesQueryWithFallbackUsing, noQueryDataMessage, - countSuccessfulQuerySeries, - collectHiddenResultIds, - mergeQueryRunResults, - appendPercentChangeSeries, } export function getQueryBuilderTimeseries({ data }: { data: QueryBuilderTimeseriesInput }) { @@ -836,10 +523,12 @@ const getQueryBuilderTimeseriesEffect = Effect.fn("QueryEngine.getQueryBuilderTi previousStartTime, previousEndTime, }, + // Reported under the wire spelling, which is what the caller sent and what + // the lab's debug panel labels its rows with. strategy: { - enableEmptyRangeFallback: strategy.enableEmptyRangeFallback, - fallbackWindowSeconds: strategy.fallbackWindowSeconds, - maxFallbackRangeSeconds: strategy.maxFallbackRangeSeconds, + enableEmptyRangeFallback: strategy.enabled, + fallbackWindowSeconds: [...strategy.windowSeconds], + maxFallbackRangeSeconds: strategy.maxRangeSeconds, }, queries: currentWindow.debug, previousQueries: previousDebug, diff --git a/packages/query-engine/package.json b/packages/query-engine/package.json index 377dcfe7f..ed57dd71a 100644 --- a/packages/query-engine/package.json +++ b/packages/query-engine/package.json @@ -8,6 +8,7 @@ "./formula-results": "./src/formula-results.ts", "./traces-shared": "./src/traces-shared.ts", "./query-builder": "./src/query-builder/model.ts", + "./query-set": "./src/query-set/index.ts", "./ch": "./src/ch/index.ts", "./profiles": "./src/profiles/index.ts", "./caching": "./src/caching/index.ts", diff --git a/packages/query-engine/src/group-key.ts b/packages/query-engine/src/group-key.ts new file mode 100644 index 000000000..ce9fde034 --- /dev/null +++ b/packages/query-engine/src/group-key.ts @@ -0,0 +1,26 @@ +/** + * The engine's name for "this result has no grouping dimension". + * + * Storage, the wire and the UI spell the same thing `UNGROUPED_GROUP_KEY` + * (`"__total__"`), and `toStorageGroupKey` in the alerts service is the one + * boundary that translates between them. That translation was already named; + * this side was a bare `"all"` repeated at a dozen sites, which is what made it + * possible to write one of them as `"__total__"` by mistake and produce an + * `alert_rule_states` row no reader can see. + * + * Lives in its own driver-free module rather than in `runtime/query-engine.ts` + * because both sides of the engine need it: the alert lowering emits it, and the + * query-set merge has to recognise it to decide whether a series gets a group + * suffix. `runtime` is API-only, so a web-bundled consumer cannot reach it. + */ +export const ENGINE_UNGROUPED_GROUP_KEY = "all" + +/** + * Whether a group name from the warehouse means "no grouping". + * + * Case-insensitive and whitespace-tolerant on purpose: this reads names that + * came back through SQL and through the raw-SQL `group` column convention, where + * the value is whatever the author's query produced. + */ +export const isEngineUngroupedKey = (groupName: string): boolean => + groupName.trim().toLowerCase() === ENGINE_UNGROUPED_GROUP_KEY diff --git a/packages/query-engine/src/index.ts b/packages/query-engine/src/index.ts index fb546d2cf..fac0e56a8 100644 --- a/packages/query-engine/src/index.ts +++ b/packages/query-engine/src/index.ts @@ -1,5 +1,6 @@ export * from "@maple/domain/query-engine" export * from "./datetime" +export * from "./group-key" export * from "./limits" export * from "@maple/domain/where-clause" export * from "./capabilities" diff --git a/packages/query-engine/src/query-set/breakdown-merge.test.ts b/packages/query-engine/src/query-set/breakdown-merge.test.ts new file mode 100644 index 000000000..43e5795fc --- /dev/null +++ b/packages/query-engine/src/query-set/breakdown-merge.test.ts @@ -0,0 +1,113 @@ +import { describe, expect, it } from "vitest" +import { type BreakdownQueryResult, mergeBreakdownResults, toDisplayName } from "./breakdown-merge" + +const queryDraft = (id: string, name: string, legend: string) => ({ id, name, legend }) + +const result = ( + queryId: string, + queryName: string, + data: Array<{ name: string; value: number }>, +): BreakdownQueryResult => ({ + queryId, + queryName, + status: "success", + error: null, + data, +}) + +describe("toDisplayName", () => { + it("prefers a non-blank legend over the name", () => { + expect(toDisplayName({ name: "A", legend: "Errors" })).toBe("Errors") + expect(toDisplayName({ name: "A", legend: " " })).toBe("A") + expect(toDisplayName({ name: "A" })).toBe("A") + }) +}) + +describe("mergeBreakdownResults", () => { + it("uses query legends as merged column names, so heatmap axes read 'Errors'/'OK' instead of 'A'/'B'", () => { + const rows = mergeBreakdownResults( + [ + result("q-a", "A", [{ name: "demo-api", value: 12 }]), + result("q-b", "B", [ + { name: "demo-api", value: 480 }, + { name: "demo-worker", value: 210 }, + ]), + ], + [queryDraft("q-a", "A", "Errors"), queryDraft("q-b", "B", "OK")], + ) + + expect(rows).toContainEqual({ name: "demo-api", Errors: 12, OK: 480 }) + expect(rows).toContainEqual({ name: "demo-worker", Errors: 0, OK: 210 }) + }) + + it("falls back to the query name when no legend is set", () => { + const rows = mergeBreakdownResults( + [result("q-a", "A", [{ name: "x", value: 1 }]), result("q-b", "B", [{ name: "x", value: 2 }])], + [queryDraft("q-a", "A", ""), queryDraft("q-b", "B", "")], + ) + + expect(rows).toEqual([{ name: "x", A: 1, B: 2 }]) + }) + + /** + * A single query keeps the narrow `{name, value}` shape. Widening it to the + * multi-query column shape would change what every pie and bar chart receives, + * which is why this merge is not folded into the timeseries one. + */ + it("returns bare name/value rows for a single query, sorted descending", () => { + const rows = mergeBreakdownResults( + [ + result("q-a", "A", [ + { name: "small", value: 1 }, + { name: "big", value: 9 }, + ]), + ], + [queryDraft("q-a", "A", "Errors")], + ) + + expect(rows).toEqual([ + { name: "big", value: 9 }, + { name: "small", value: 1 }, + ]) + }) + + it("orders multi-query rows by the first query's values", () => { + const rows = mergeBreakdownResults( + [ + result("q-a", "A", [ + { name: "low", value: 1 }, + { name: "high", value: 100 }, + ]), + result("q-b", "B", [ + { name: "low", value: 999 }, + { name: "high", value: 2 }, + ]), + ], + [queryDraft("q-a", "A", ""), queryDraft("q-b", "B", "")], + ) + + expect(rows.map((row) => row.name)).toEqual(["high", "low"]) + }) + + it("ignores failed and empty results", () => { + const rows = mergeBreakdownResults( + [ + { queryId: "q-a", queryName: "A", status: "error", error: "boom", data: [] }, + result("q-b", "B", [{ name: "x", value: 2 }]), + ], + [queryDraft("q-a", "A", ""), queryDraft("q-b", "B", "")], + ) + + // Only one successful result survives, so it takes the narrow shape. + expect(rows).toEqual([{ name: "x", value: 2 }]) + }) + + it("returns nothing when no query succeeded with data", () => { + expect( + mergeBreakdownResults( + [{ queryId: "q-a", queryName: "A", status: "error", error: "boom", data: [] }], + [queryDraft("q-a", "A", "")], + ), + ).toEqual([]) + }) +}) diff --git a/packages/query-engine/src/query-set/breakdown-merge.ts b/packages/query-engine/src/query-set/breakdown-merge.ts new file mode 100644 index 000000000..d1c19f712 --- /dev/null +++ b/packages/query-engine/src/query-set/breakdown-merge.ts @@ -0,0 +1,75 @@ +/** + * Folding N breakdown results into chart-ready rows. + * + * Pure. Deliberately a different shape from the timeseries merge in + * `series-merge.ts`: a breakdown has no bucket axis, so a single query returns + * bare `{name, value}` rows and only a multi-query set widens into one column + * per query. Collapsing the two merges into one would force every single-query + * breakdown through the wide shape and change what every pie and bar chart + * receives. + */ + +/** One query's breakdown outcome. Failures are values, not thrown — see `runBreakdownQuerySet`. */ +export interface BreakdownQueryResult { + readonly queryId: string + readonly queryName: string + readonly status: "success" | "error" + readonly error: string | null + readonly data: ReadonlyArray<{ name: string; value: number }> +} + +/** The label a query draws under: its legend if set, else its name. */ +export function toDisplayName(query: { name: string; legend?: string }): string { + const trimmedLegend = (query.legend ?? "").trim() + return trimmedLegend || query.name +} + +export function mergeBreakdownResults( + results: ReadonlyArray, + enabledQueries: ReadonlyArray<{ id: string; name: string; legend?: string }>, +): Array> { + const successful = results.filter((r) => r.status === "success" && r.data.length > 0) + if (successful.length === 0) return [] + + // Single query: return simple { name, value } rows + if (successful.length === 1) { + return successful[0].data + .map((item) => ({ name: item.name, value: item.value })) + .sort((a, b) => b.value - a.value) + } + + const rowsByName = new Map>() + const columnNames: string[] = [] + const queriesById = new Map(enabledQueries.map((q) => [q.id, q])) + + for (const result of successful) { + const query = queriesById.get(result.queryId) + const displayName = query ? toDisplayName(query) : result.queryName + columnNames.push(displayName) + + for (const item of result.data) { + const row = rowsByName.get(item.name) ?? { name: item.name } + row[displayName] = item.value + rowsByName.set(item.name, row) + } + } + + // Zero-fill, same reason as the timeseries merge: an absent column reads as + // missing data rather than as no events. + for (const row of rowsByName.values()) { + for (const col of columnNames) { + if (typeof row[col] !== "number") { + row[col] = 0 + } + } + } + + // Ordered by the first query's values — the one the author added first, which + // is the one a reader treats as the subject of the chart. + const firstCol = columnNames[0] + return Array.from(rowsByName.values()).sort((a, b) => { + const aVal = typeof a[firstCol] === "number" ? a[firstCol] : 0 + const bVal = typeof b[firstCol] === "number" ? b[firstCol] : 0 + return bVal - aVal + }) +} diff --git a/packages/query-engine/src/query-set/bucketing.test.ts b/packages/query-engine/src/query-set/bucketing.test.ts new file mode 100644 index 000000000..9add9da2c --- /dev/null +++ b/packages/query-engine/src/query-set/bucketing.test.ts @@ -0,0 +1,187 @@ +import { describe, expect, it } from "vitest" +import type { QuerySpec } from "@maple/domain/query-engine" +import { + buildExecutionWindows, + LAB_EMPTY_RANGE_STRATEGY, + NO_EMPTY_RANGE_FALLBACK, + resolveExecutionSpecForWindow, + resolveFallbackStrategy, + resolveTimeseriesBucketSpec, +} from "./bucketing" + +describe("resolveTimeseriesBucketSpec", () => { + it("resolves deterministic auto bucket seconds for timeseries specs", () => { + const spec: QuerySpec = { + kind: "timeseries", + source: "traces", + metric: "count", + groupBy: ["service"], + } + + const resolved = resolveTimeseriesBucketSpec(spec, "2026-01-01 00:00:00", "2026-01-02 00:00:00") + + expect(resolved.kind).toBe("timeseries") + if (resolved.kind !== "timeseries") return + expect(resolved.bucketSeconds).toBe(900) + }) + + it("does not mutate explicit bucket seconds", () => { + const spec: QuerySpec = { + kind: "timeseries", + source: "logs", + metric: "count", + bucketSeconds: 900, + } + + expect(resolveTimeseriesBucketSpec(spec, "2026-01-01 00:00:00", "2026-01-01 03:00:00")).toEqual(spec) + }) + + it("leaves a non-timeseries spec alone", () => { + const spec: QuerySpec = { + kind: "breakdown", + source: "traces", + metric: "count", + groupBy: "service", + } + expect(resolveTimeseriesBucketSpec(spec, "2026-01-01 00:00:00", "2026-01-02 00:00:00")).toEqual(spec) + }) +}) + +describe("resolveExecutionSpecForWindow", () => { + it("resolves auto bucket per execution window (primary + fallback)", () => { + const spec: QuerySpec = { kind: "timeseries", source: "traces", metric: "count" } + + const primary = resolveExecutionSpecForWindow(spec, { + startTime: "2026-01-02 00:00:00", + endTime: "2026-01-02 01:00:00", + kind: "primary", + }) + const fallback = resolveExecutionSpecForWindow(spec, { + startTime: "2026-01-01 01:00:00", + endTime: "2026-01-02 01:00:00", + kind: "fallback", + }) + + expect(primary.kind).toBe("timeseries") + expect(fallback.kind).toBe("timeseries") + if (primary.kind !== "timeseries" || fallback.kind !== "timeseries") return + + expect(primary.bucketSeconds).toBe(60) + expect(fallback.bucketSeconds).toBe(900) + }) + + it("widens explicit bucket on fallback windows to stay within point budget", () => { + const spec: QuerySpec = { + kind: "timeseries", + source: "traces", + metric: "count", + bucketSeconds: 60, + } + + const primary = resolveExecutionSpecForWindow(spec, { + startTime: "2026-01-02 00:00:00", + endTime: "2026-01-02 01:00:00", + kind: "primary", + }) + const fallback = resolveExecutionSpecForWindow(spec, { + startTime: "2026-01-01 01:00:00", + endTime: "2026-01-02 01:00:00", + kind: "fallback", + }) + + expect(primary.kind).toBe("timeseries") + expect(fallback.kind).toBe("timeseries") + if (primary.kind !== "timeseries" || fallback.kind !== "timeseries") return + + // The primary keeps the author's 60s; only the wider fallback coarsens, or a + // 31-day window at 60s would ask for ~45k points. + expect(primary.bucketSeconds).toBe(60) + expect(fallback.bucketSeconds).toBe(900) + }) +}) + +describe("buildExecutionWindows", () => { + it("builds deterministic fallback execution windows", () => { + const windows = buildExecutionWindows( + "2026-01-02 00:00:00", + "2026-01-02 01:00:00", + { enabled: true, windowSeconds: [86400], maxRangeSeconds: 86400 * 31 }, + true, + ) + + expect(windows).toEqual([ + { startTime: "2026-01-02 00:00:00", endTime: "2026-01-02 01:00:00", kind: "primary" }, + { startTime: "2026-01-01 01:00:00", endTime: "2026-01-02 01:00:00", kind: "fallback" }, + ]) + }) + + it("returns only the primary when the strategy is off", () => { + expect( + buildExecutionWindows( + "2026-01-02 00:00:00", + "2026-01-02 01:00:00", + NO_EMPTY_RANGE_FALLBACK, + true, + ), + ).toHaveLength(1) + }) + + it("returns only the primary when the caller disallows fallback", () => { + // This is the previous-period window: widening it would compare the current + // window against a differently-sized one. + expect( + buildExecutionWindows( + "2026-01-02 00:00:00", + "2026-01-02 01:00:00", + LAB_EMPTY_RANGE_STRATEGY, + false, + ), + ).toHaveLength(1) + }) + + it("only ever widens, and never past the ceiling", () => { + const windows = buildExecutionWindows( + "2026-01-01 00:00:00", + "2026-01-08 00:00:00", // already 7 days + LAB_EMPTY_RANGE_STRATEGY, + true, + ) + + // 24h and 7d are not wider than the request; only 31d survives. + expect(windows).toHaveLength(2) + expect(windows[1].startTime).toBe("2025-12-08 00:00:00") + }) + + it("falls back to the primary alone for an unparseable or inverted range", () => { + expect( + buildExecutionWindows("nonsense", "2026-01-02 01:00:00", LAB_EMPTY_RANGE_STRATEGY, true), + ).toEqual([{ startTime: "nonsense", endTime: "2026-01-02 01:00:00", kind: "primary" }]) + expect( + buildExecutionWindows( + "2026-01-02 01:00:00", + "2026-01-02 00:00:00", + LAB_EMPTY_RANGE_STRATEGY, + true, + ), + ).toHaveLength(1) + }) +}) + +describe("resolveFallbackStrategy", () => { + it("sorts, dedupes and drops non-positive rungs", () => { + expect( + resolveFallbackStrategy({ windowSeconds: [7200, 60, 7200, 0, -5, Number.NaN] }).windowSeconds, + ).toEqual([60, 7200]) + }) + + it("inherits from the base for absent fields", () => { + expect(resolveFallbackStrategy(undefined)).toEqual(LAB_EMPTY_RANGE_STRATEGY) + expect(resolveFallbackStrategy({}, NO_EMPTY_RANGE_FALLBACK).enabled).toBe(false) + }) + + it("lets an explicit `enabled: false` override an enabled base", () => { + // Every dashboard tile relies on this: it sends `enabled: false` and must + // not inherit the lab ladder. + expect(resolveFallbackStrategy({ enabled: false }, LAB_EMPTY_RANGE_STRATEGY).enabled).toBe(false) + }) +}) diff --git a/packages/query-engine/src/query-set/bucketing.ts b/packages/query-engine/src/query-set/bucketing.ts new file mode 100644 index 000000000..129e45238 --- /dev/null +++ b/packages/query-engine/src/query-set/bucketing.ts @@ -0,0 +1,163 @@ +/** + * Choosing the bucket width and the windows a query set actually executes over. + * + * Pure. The empty-range fallback lives here rather than in the runner because it + * is a *policy* — see `EmptyRangeFallbackStrategy` below for why it is opt-in. + */ + +import type { QuerySpec } from "@maple/domain/query-engine" +import { computeBucketSecondsForRange, formatWarehouseDateTime, parseWarehouseDateTime } from "../datetime" + +export interface ExecutionWindow { + readonly startTime: string + readonly endTime: string + readonly kind: "primary" | "fallback" +} + +/** + * Widen the window and retry when the requested range came back empty. + * + * OPT-IN, and off unless a caller asks — which matches what already happens: + * every dashboard tile passes `enableEmptyRangeFallback: false`, so only the + * query-builder lab and the metric detail page ever use it. Those are explore + * surfaces, where "show me the most recent data that exists" is the useful + * answer to an empty window. + * + * It must never reach alert evaluation. An alert's window IS its semantics: + * silently evaluating 24h because the last 5 minutes were empty would breach or + * clear on data outside the rule's window, would break `minimumSampleCount` + * (which counts samples in the rule window), and would make + * `noDataBehavior: "zero"` unreachable — "the window is empty" is exactly the + * signal a throughput-drop alert exists to catch. That is enforced structurally: + * nothing on the alert path takes this type. + */ +export interface EmptyRangeFallbackStrategy { + readonly enabled: boolean + readonly windowSeconds: ReadonlyArray + readonly maxRangeSeconds: number +} + +/** No widening: what every dashboard tile already asks for. */ +export const NO_EMPTY_RANGE_FALLBACK: EmptyRangeFallbackStrategy = { + enabled: false, + windowSeconds: [], + maxRangeSeconds: 0, +} + +/** The 24h → 7d → 31d ladder the explore surfaces use. */ +export const LAB_EMPTY_RANGE_STRATEGY: EmptyRangeFallbackStrategy = { + enabled: true, + windowSeconds: [24 * 60 * 60, 7 * 24 * 60 * 60, 31 * 24 * 60 * 60], + maxRangeSeconds: 31 * 24 * 60 * 60, +} + +/** Normalize a partially-specified strategy: drop non-positive rungs, sort, dedupe. */ +export function resolveFallbackStrategy( + input: + | { + readonly enabled?: boolean + readonly windowSeconds?: ReadonlyArray + readonly maxRangeSeconds?: number + } + | undefined, + base: EmptyRangeFallbackStrategy = LAB_EMPTY_RANGE_STRATEGY, +): EmptyRangeFallbackStrategy { + const uniqueWindows = new Set( + (input?.windowSeconds ?? base.windowSeconds).filter( + (seconds) => Number.isFinite(seconds) && seconds > 0, + ), + ) + + return { + enabled: input?.enabled ?? base.enabled, + windowSeconds: Array.from(uniqueWindows).sort((left, right) => left - right), + maxRangeSeconds: input?.maxRangeSeconds ?? base.maxRangeSeconds, + } +} + +/** Fill in `bucketSeconds` from the range when the lowering left it unset. */ +export function resolveTimeseriesBucketSpec(spec: QuerySpec, startTime: string, endTime: string): QuerySpec { + if (spec.kind !== "timeseries" || spec.bucketSeconds) { + return spec + } + + return { + ...spec, + bucketSeconds: computeBucketSecondsForRange(startTime, endTime, "chart"), + } satisfies QuerySpec +} + +/** + * The spec for one execution window. + * + * A fallback window is wider than the primary, so it takes the COARSER of the + * two bucket widths — reusing the primary's width over a 31-day window would ask + * for tens of thousands of points. + */ +export function resolveExecutionSpecForWindow(spec: QuerySpec, window: ExecutionWindow): QuerySpec { + const resolved = resolveTimeseriesBucketSpec(spec, window.startTime, window.endTime) + if (resolved.kind !== "timeseries") { + return resolved + } + + if (window.kind !== "fallback") { + return resolved + } + + const autoBucketSeconds = computeBucketSecondsForRange(window.startTime, window.endTime, "chart") + const selectedBucketSeconds = Math.max(resolved.bucketSeconds ?? autoBucketSeconds, autoBucketSeconds) + return { + ...resolved, + bucketSeconds: selectedBucketSeconds, + } +} + +/** + * The ordered windows to try: the requested one, then progressively wider ones + * if the strategy allows. Always at least the primary. + */ +export function buildExecutionWindows( + startTime: string, + endTime: string, + strategy: EmptyRangeFallbackStrategy, + allowFallback: boolean, +): ExecutionWindow[] { + const startMs = parseWarehouseDateTime(startTime) + const endMs = parseWarehouseDateTime(endTime) + if (Number.isNaN(startMs) || Number.isNaN(endMs) || endMs <= startMs) { + return [{ startTime, endTime, kind: "primary" }] + } + + const rangeSeconds = Math.max((endMs - startMs) / 1000, 1) + const windows: ExecutionWindow[] = [{ startTime, endTime, kind: "primary" }] + + if (!allowFallback || !strategy.enabled) { + return windows + } + + const seen = new Set([`${startTime}|${endTime}`]) + for (const seconds of strategy.windowSeconds) { + // Only ever widen, and never past the ceiling. + if (seconds <= rangeSeconds || seconds > strategy.maxRangeSeconds) { + continue + } + + const windowStartMs = endMs - seconds * 1000 + const nextStart = formatWarehouseDateTime(windowStartMs) + const nextEnd = formatWarehouseDateTime(endMs) + const key = `${nextStart}|${nextEnd}` + + if (seen.has(key)) { + continue + } + + seen.add(key) + windows.push({ + startTime: nextStart, + endTime: nextEnd, + kind: "fallback", + }) + } + + return windows +} diff --git a/packages/query-engine/src/query-set/index.ts b/packages/query-engine/src/query-set/index.ts new file mode 100644 index 000000000..575a056e3 --- /dev/null +++ b/packages/query-engine/src/query-set/index.ts @@ -0,0 +1,25 @@ +// @maple/query-engine/query-set — running a stored QuerySet and shaping the result. +// +// The layer between the pure lowering (`../query-builder/model`, draft → +// QuerySpec) and a host that can execute a QuerySpec. Three surfaces needed +// exactly this and each had grown its own copy: the web app's +// `query-builder-timeseries` server function, the MCP widget inspector, and the +// alert rule compiler. +// +// Its own subpath rather than `./runtime` or the root barrel, for one concrete +// reason: `apps/web/src/api/warehouse/*` runs in the BROWSER, so everything here +// is browser-bundled. `./runtime` pulls the ClickHouse DSL and is API-only; the +// root barrel is imported for small helpers all over the place and should not +// drag the merge machinery in behind them. +// +// Deliberately NOT here: +// - Alert bucket observations. `computeAlertBuckets` in `../runtime` emits a +// per-(bucket, group) `sampleCount` that `QueryEngineResult` does not carry, +// and `minimumSampleCount` depends on it. The runner and that lowering are +// two shaping layers over the same lowering, not one over the other. +// - Raw SQL. It is not a query set; it goes through `executeRawSql` with its +// own reshaping. + +export * from "./breakdown-merge" +export * from "./bucketing" +export * from "./series-merge" diff --git a/packages/query-engine/src/query-set/series-merge.test.ts b/packages/query-engine/src/query-set/series-merge.test.ts new file mode 100644 index 000000000..712e7ee7f --- /dev/null +++ b/packages/query-engine/src/query-set/series-merge.test.ts @@ -0,0 +1,348 @@ +import { describe, expect, it } from "vitest" +import type { QueryRunResult } from "../formula-results" +import { + appendPercentChangeSeries, + collectHiddenResultIds, + combineRows, + countSuccessfulQuerySeries, + hasAnySeriesData, + mergeQueryRunResults, + shiftResultPoints, + shiftRunResults, + toDisplayNameById, +} from "./series-merge" + +function makeQueryResult(overrides: Partial = {}): QueryRunResult { + return { + queryId: "q-1", + queryName: "A", + source: "traces", + status: "success", + error: null, + warnings: [], + data: [], + ...overrides, + } +} + +describe("hasAnySeriesData / countSuccessfulQuerySeries", () => { + it("treats an empty series map as no data", () => { + expect(hasAnySeriesData([{ bucket: "b", series: {} }])).toBe(false) + expect(hasAnySeriesData([{ bucket: "b", series: { total: 0 } }])).toBe(true) + }) + + it("counts only query results with real series data", () => { + const count = countSuccessfulQuerySeries([ + makeQueryResult({ data: [{ bucket: "2026-01-01T00:00:00.000Z", series: {} }] }), + makeQueryResult({ + queryId: "q-2", + queryName: "B", + data: [{ bucket: "2026-01-01T00:00:00.000Z", series: { total: 1 } }], + }), + ]) + + expect(count).toBe(1) + }) +}) + +describe("toDisplayNameById", () => { + it("prefers a non-blank legend over the name", () => { + const map = toDisplayNameById([ + { id: "a", name: "Query A", legend: "Errors" }, + { id: "b", name: "Query B", legend: " " }, + { id: "c", name: "Query C" }, + ]) + + expect(map.get("a")).toBe("Errors") + expect(map.get("b")).toBe("Query B") + expect(map.get("c")).toBe("Query C") + }) +}) + +describe("mergeQueryRunResults", () => { + it("preserves grouped series instead of summing them per query", () => { + const merged = mergeQueryRunResults( + [ + makeQueryResult({ + queryId: "q-1", + queryName: "A", + data: [ + { bucket: "2026-01-01T00:00:00.000Z", series: { checkout: 2, billing: 1 } }, + { bucket: "2026-01-01T00:05:00.000Z", series: { checkout: 4 } }, + ], + }), + makeQueryResult({ + queryId: "q-2", + queryName: "B", + data: [ + { bucket: "2026-01-01T00:00:00.000Z", series: { checkout: 5 } }, + { bucket: "2026-01-01T00:05:00.000Z", series: { checkout: 7 } }, + ], + }), + ], + new Map([ + ["q-1", "Errors"], + ["q-2", "Throughput"], + ]), + ) + + expect(merged.seriesNames).toEqual(["Errors: checkout", "Errors: billing", "Throughput: checkout"]) + + expect(merged.rowsByBucket.get("2026-01-01T00:00:00.000Z")).toEqual({ + bucket: "2026-01-01T00:00:00.000Z", + "Errors: checkout": 2, + "Errors: billing": 1, + "Throughput: checkout": 5, + }) + // Zero-filled: `Errors: billing` has no row in the second bucket, and a gap + // would read as missing data rather than as no events. + expect(merged.rowsByBucket.get("2026-01-01T00:05:00.000Z")).toEqual({ + bucket: "2026-01-01T00:05:00.000Z", + "Errors: checkout": 4, + "Errors: billing": 0, + "Throughput: checkout": 7, + }) + }) + + it("keeps non-grouped 'all' series as the display name", () => { + const merged = mergeQueryRunResults( + [ + makeQueryResult({ + queryId: "q-1", + queryName: "A", + data: [{ bucket: "2026-01-01T00:00:00.000Z", series: { all: 12 } }], + }), + ], + new Map([["q-1", "Requests"]]), + ) + + expect(merged.seriesNames).toEqual(["Requests"]) + expect(merged.rowsByBucket.get("2026-01-01T00:00:00.000Z")).toEqual({ + bucket: "2026-01-01T00:00:00.000Z", + Requests: 12, + }) + }) + + it("recognises the ungrouped key case-insensitively", () => { + const merged = mergeQueryRunResults( + [ + makeQueryResult({ + queryId: "q-1", + data: [{ bucket: "2026-01-01T00:00:00.000Z", series: { All: 3 } }], + }), + ], + new Map([["q-1", "Requests"]]), + ) + + expect(merged.seriesNames).toEqual(["Requests"]) + }) + + it("keeps formula series labels without redundant namespacing", () => { + const merged = mergeQueryRunResults( + [ + makeQueryResult({ + queryId: "f-1", + queryName: "F1", + source: "formula", + data: [{ bucket: "2026-01-01T00:00:00.000Z", series: { "Error ratio": 0.3 } }], + }), + ], + new Map([["f-1", "Error ratio"]]), + ) + + expect(merged.seriesNames).toEqual(["Error ratio"]) + expect(merged.rowsByBucket.get("2026-01-01T00:00:00.000Z")).toEqual({ + bucket: "2026-01-01T00:00:00.000Z", + "Error ratio": 0.3, + }) + }) + + it("skips non-finite values rather than writing NaN into a row", () => { + const merged = mergeQueryRunResults( + [ + makeQueryResult({ + queryId: "q-1", + data: [ + { + bucket: "2026-01-01T00:00:00.000Z", + series: { good: 1, bad: Number.NaN, worse: Number.POSITIVE_INFINITY }, + }, + ], + }), + ], + new Map([["q-1", "A"]]), + ) + + expect(merged.seriesNames).toEqual(["good"]) + }) + + /** + * The shared `usedSeriesNames` set is what keeps the previous-period window + * from claiming a name the current window already took. It also makes these + * two calls ORDER-DEPENDENT, which is the property this asserts: merge the + * current window first, or ` (prev)` series would take the unsuffixed names. + */ + it("does not let a second merge reuse the first merge's series names", () => { + const usedSeriesNames = new Set() + const displayNameById = new Map([["q-1", "Requests"]]) + const result = makeQueryResult({ + queryId: "q-1", + data: [{ bucket: "2026-01-01T00:00:00.000Z", series: { all: 1 } }], + }) + + const current = mergeQueryRunResults([result], displayNameById, { usedSeriesNames }) + const previous = mergeQueryRunResults([result], displayNameById, { + seriesSuffix: " (prev)", + usedSeriesNames, + }) + + expect(current.seriesNames).toEqual(["Requests"]) + expect(previous.seriesNames).toEqual(["Requests (prev)"]) + }) + + it("disambiguates a genuine name collision with a numeric suffix", () => { + const merged = mergeQueryRunResults( + [ + makeQueryResult({ + queryId: "q-1", + data: [{ bucket: "b", series: { all: 1 } }], + }), + makeQueryResult({ + queryId: "q-2", + data: [{ bucket: "b", series: { all: 2 } }], + }), + ], + new Map([ + ["q-1", "Requests"], + ["q-2", "Requests"], + ]), + ) + + expect(merged.seriesNames).toEqual(["Requests", "Requests (2)"]) + }) +}) + +describe("combineRows", () => { + it("merges current and previous sets and sorts by bucket", () => { + const rows = combineRows([ + { + rowsByBucket: new Map([ + ["2026-01-01T01:00:00.000Z", { bucket: "2026-01-01T01:00:00.000Z", A: 2 }], + ["2026-01-01T00:00:00.000Z", { bucket: "2026-01-01T00:00:00.000Z", A: 1 }], + ]), + seriesNames: ["A"], + }, + { + rowsByBucket: new Map([ + ["2026-01-01T00:00:00.000Z", { bucket: "2026-01-01T00:00:00.000Z", "A (prev)": 5 }], + ]), + seriesNames: ["A (prev)"], + }, + ]) + + expect(rows.map((row) => row.bucket)).toEqual([ + "2026-01-01T00:00:00.000Z", + "2026-01-01T01:00:00.000Z", + ]) + // The bucket the previous set had no row for is zero-filled, not absent. + expect(rows[1]).toEqual({ bucket: "2026-01-01T01:00:00.000Z", A: 2, "A (prev)": 0 }) + }) +}) + +describe("shiftResultPoints / shiftRunResults", () => { + it("moves a previous-period bucket forward onto the current window", () => { + const shifted = shiftResultPoints( + [{ bucket: "2026-01-01T00:00:00.000Z", series: { all: 1 } }], + 3600_000, + ) + expect(shifted[0].bucket).toBe("2026-01-01T01:00:00.000Z") + }) + + it("passes an unparseable bucket through rather than producing Invalid Date", () => { + const shifted = shiftResultPoints([{ bucket: "not-a-date", series: {} }], 1000) + expect(shifted[0].bucket).toBe("not-a-date") + }) + + it("shifts every result's points and keeps the rest of the result", () => { + const shifted = shiftRunResults( + [ + makeQueryResult({ + warnings: ["w"], + data: [{ bucket: "2026-01-01T00:00:00.000Z", series: { all: 1 } }], + }), + ], + 3600_000, + ) + + expect(shifted[0].warnings).toEqual(["w"]) + expect(shifted[0].data[0].bucket).toBe("2026-01-01T01:00:00.000Z") + }) +}) + +describe("appendPercentChangeSeries", () => { + it("computes percent change per stable grouped series", () => { + const rows: Array> = [ + { + bucket: "2026-01-01T00:00:00.000Z", + "Errors: checkout": 20, + "Errors: checkout (prev)": 10, + }, + ] + + appendPercentChangeSeries( + rows, + new Map([["q-1::checkout", "Errors: checkout"]]), + new Map([["q-1::checkout", "Errors: checkout (prev)"]]), + ) + + expect(rows[0]["Errors: checkout (%Δ)"]).toBe(100) + }) + + it("prev=0 & cur=0 is 0% (genuinely unchanged); prev=0 & cur>0 leaves a gap, not a fake 0%", () => { + const rows: Array> = [ + { + bucket: "2026-01-01T00:00:00.000Z", + "Errors: checkout": 0, + "Errors: checkout (prev)": 0, + }, + { + bucket: "2026-01-01T01:00:00.000Z", + "Errors: checkout": 5, + "Errors: checkout (prev)": 0, + }, + ] + + appendPercentChangeSeries( + rows, + new Map([["q-1::checkout", "Errors: checkout"]]), + new Map([["q-1::checkout", "Errors: checkout (prev)"]]), + ) + + expect(rows[0]["Errors: checkout (%Δ)"]).toBe(0) + expect(rows[1]).not.toHaveProperty("Errors: checkout (%Δ)") + }) + + it("skips a series with no previous-period twin", () => { + const rows: Array> = [{ bucket: "b", A: 5 }] + appendPercentChangeSeries(rows, new Map([["q-1::all", "A"]]), new Map()) + expect(rows[0]).toEqual({ bucket: "b", A: 5 }) + }) +}) + +describe("collectHiddenResultIds", () => { + // A ratio widget hides its numerator/denominator queries and plots only the formula. Merging + // the hidden operands in anyway put raw counts on the same axis as a 0–1 ratio — and, under + // the widget's own `percent` unit, drew them as "416849856400.0%". + it("collects hidden query and formula ids so they are dropped before merging", () => { + const hidden = collectHiddenResultIds({ + queries: [{ id: "num", hidden: true }, { id: "den", hidden: true }, { id: "plain" }], + formulas: [{ id: "ratio" }, { id: "scratch", hidden: true }], + }) + + expect([...hidden].sort()).toEqual(["den", "num", "scratch"]) + }) + + it("treats a widget with no formulas and nothing hidden as fully plotted", () => { + expect(collectHiddenResultIds({ queries: [{ id: "a" }, { id: "b" }] }).size).toBe(0) + }) +}) diff --git a/packages/query-engine/src/query-set/series-merge.ts b/packages/query-engine/src/query-set/series-merge.ts new file mode 100644 index 000000000..120375d4a --- /dev/null +++ b/packages/query-engine/src/query-set/series-merge.ts @@ -0,0 +1,289 @@ +/** + * Turning per-query timeseries results into the flat, chart-ready rows a caller + * renders: one row per bucket, one column per series. + * + * Pure — no Effect, no port, no warehouse. Lifted out of the web app's + * `query-builder-timeseries` server function unchanged, because the same merge + * was needed by the MCP widget inspector and would have been copied a third + * time. The behaviours preserved verbatim here are load-bearing and each is + * commented at its site. + */ + +import type { QueryRunResult, TimeseriesPoint } from "../formula-results" +import { isEngineUngroupedKey } from "../group-key" + +/** A point carries data when its `series` map is non-empty. */ +export function hasAnySeriesData(points: ReadonlyArray): boolean { + return points.some((point) => Object.keys(point.series).length > 0) +} + +export function countSuccessfulQuerySeries(results: ReadonlyArray): number { + return results.filter((result) => result.status === "success" && hasAnySeriesData(result.data)).length +} + +/** + * Ids of queries and formulas whose series must not be plotted. + * + * `hidden` means "feed the formulas, don't draw me" — a hidden query still runs, because the + * formula that references it needs its numbers. The query-builder UI has always honored the flag + * (`widget-builder-utils`) but this data source did not, so a saved widget built on a hidden + * numerator/denominator plotted its raw operands next to the formula. On a ratio widget that also + * means raw counts rendered with the ratio's unit — the Cloudflare cache-hit chart drew + * "416849856400.0%" beside its real 0–1 hit rate. + */ +export function collectHiddenResultIds(input: { + queries: ReadonlyArray<{ id: string; hidden?: boolean }> + formulas?: ReadonlyArray<{ id: string; hidden?: boolean }> +}): Set { + return new Set([ + ...input.queries.filter((query) => query.hidden).map((query) => query.id), + ...(input.formulas ?? []).filter((formula) => formula.hidden).map((formula) => formula.id), + ]) +} + +/** The label each query/formula draws under: its legend if set, else its name. */ +export function toDisplayNameById( + entries: ReadonlyArray<{ id: string; name: string; legend?: string }>, +): Map { + const map = new Map() + + for (const entry of entries) { + const trimmedLegend = (entry.legend ?? "").trim() + map.set(entry.id, trimmedLegend || entry.name) + } + + return map +} + +/** + * How one (query, group) pair names its series, and the key it is stable under + * across the current and previous comparison windows. + * + * The `stableGroupKey` is what lets `appendPercentChangeSeries` pair a series + * with its own shifted counterpart — matching on the *label* would fail the + * moment the label got a ` (prev)` suffix or a `(2)` disambiguator. + */ +export function toSeriesDescriptor( + result: QueryRunResult, + displayName: string, + rawGroupName: string, + singleQuery: boolean, +): { + stableGroupKey: string + seriesLabel: string +} { + const normalizedGroupName = rawGroupName.trim() || "unnamed" + const isAllGroup = isEngineUngroupedKey(normalizedGroupName) + const isFormulaSelfNamed = result.source === "formula" && normalizedGroupName === displayName + + if (isAllGroup || isFormulaSelfNamed) { + return { + stableGroupKey: "__all__", + seriesLabel: displayName, + } + } + + return { + stableGroupKey: normalizedGroupName, + seriesLabel: singleQuery ? normalizedGroupName : `${displayName}: ${normalizedGroupName}`, + } +} + +export interface MergedSeries { + rowsByBucket: Map> + seriesNameByStableKey: Map + seriesNames: string[] +} + +/** + * Fold N successful query results into bucket-keyed rows. + * + * `options.usedSeriesNames` is a MUTABLE set the caller owns, and passing the + * same one across two calls is how the previous-period window avoids colliding + * with the current window's names. That makes this call ORDER-DEPENDENT: the + * current window must be merged before the previous one, or the ` (prev)` series + * would claim the unsuffixed names first. + */ +export function mergeQueryRunResults( + results: ReadonlyArray, + displayNameById: Map, + options?: { + seriesSuffix?: string + usedSeriesNames?: Set + }, +): MergedSeries { + const rowsByBucket = new Map>() + const usedSeriesNames = options?.usedSeriesNames ?? new Set() + const seriesNameByStableKey = new Map() + const seriesNames: string[] = [] + const suffix = options?.seriesSuffix ?? "" + + const uniqueName = (base: string): string => { + if (!usedSeriesNames.has(base)) { + usedSeriesNames.add(base) + return base + } + + let suffix = 2 + while (usedSeriesNames.has(`${base} (${suffix})`)) { + suffix += 1 + } + + const next = `${base} (${suffix})` + usedSeriesNames.add(next) + return next + } + + // A single plotted query labels its series by group name alone; two or more + // prefix the query's display name, or "500" from one query and "500" from + // another would collide into one column. + const successfulResultCount = results.filter( + (r) => r.status === "success" && r.data.length > 0 && hasAnySeriesData(r.data), + ).length + const singleQuery = successfulResultCount <= 1 + + for (const result of results) { + if (result.status !== "success") { + continue + } + + if (result.data.length === 0 || !hasAnySeriesData(result.data)) { + continue + } + + const preferredName = displayNameById.get(result.queryId) ?? result.queryName + + for (const point of result.data) { + const row = rowsByBucket.get(point.bucket) ?? { bucket: point.bucket } + if (Object.keys(point.series).length > 0) { + for (const [groupName, rawValue] of Object.entries(point.series)) { + const value = typeof rawValue === "number" ? rawValue : Number(rawValue) + if (!Number.isFinite(value)) { + continue + } + + const descriptor = toSeriesDescriptor(result, preferredName, groupName, singleQuery) + const stableKey = `${result.queryId}::${descriptor.stableGroupKey}` + let seriesName = seriesNameByStableKey.get(stableKey) + + if (!seriesName) { + seriesName = uniqueName(`${descriptor.seriesLabel}${suffix}`) + seriesNameByStableKey.set(stableKey, seriesName) + seriesNames.push(seriesName) + } + + row[seriesName] = value + } + } + rowsByBucket.set(point.bucket, row) + } + } + + // Zero-fill: a chart library reading `undefined` for a series in one bucket + // draws a gap, which reads as missing data rather than as no events. + for (const row of rowsByBucket.values()) { + for (const seriesName of seriesNames) { + if (typeof row[seriesName] !== "number") { + row[seriesName] = 0 + } + } + } + + return { + rowsByBucket, + seriesNameByStableKey, + seriesNames, + } +} + +/** Flatten one or more merged sets (current, previous) into sorted rows. */ +export function combineRows( + mergedSets: ReadonlyArray<{ + rowsByBucket: Map> + seriesNames: string[] + }>, +): Array> { + const rowsByBucket = new Map>() + const allSeriesNames = new Set() + + for (const merged of mergedSets) { + for (const seriesName of merged.seriesNames) { + allSeriesNames.add(seriesName) + } + + for (const [bucket, row] of merged.rowsByBucket.entries()) { + const existing = rowsByBucket.get(bucket) ?? { bucket } + rowsByBucket.set(bucket, { ...existing, ...row }) + } + } + + for (const row of rowsByBucket.values()) { + for (const seriesName of allSeriesNames) { + if (typeof row[seriesName] !== "number") { + row[seriesName] = 0 + } + } + } + + return Array.from(rowsByBucket.values()).sort((left, right) => + String(left.bucket).localeCompare(String(right.bucket)), + ) +} + +function shiftBucket(bucket: string, offsetMs: number): string { + const parsed = new Date(bucket).getTime() + if (Number.isNaN(parsed)) { + return bucket + } + + return new Date(parsed + offsetMs).toISOString() +} + +/** Move a previous-period result forward onto the current window's buckets. */ +export function shiftResultPoints( + points: ReadonlyArray, + offsetMs: number, +): TimeseriesPoint[] { + return points.map((point) => ({ + bucket: shiftBucket(point.bucket, offsetMs), + series: { ...point.series }, + })) +} + +export function shiftRunResults(results: ReadonlyArray, shiftMs: number): QueryRunResult[] { + return results.map((result) => ({ + ...result, + data: shiftResultPoints(result.data, shiftMs), + })) +} + +/** Append a `(%Δ)` series for every series that has a previous-period twin. */ +export function appendPercentChangeSeries( + rows: ReadonlyArray>, + currentSeriesByStableKey: Map, + previousSeriesByStableKey: Map, +): void { + for (const [stableKey, currentSeriesName] of currentSeriesByStableKey.entries()) { + const previousSeriesName = previousSeriesByStableKey.get(stableKey) + if (!previousSeriesName) { + continue + } + + const deltaSeriesName = `${currentSeriesName} (%Δ)` + for (const row of rows) { + const current = row[currentSeriesName] + const previous = row[previousSeriesName] + + const currentValue = typeof current === "number" && Number.isFinite(current) ? current : 0 + const previousValue = typeof previous === "number" && Number.isFinite(previous) ? previous : 0 + + // prev=0 & cur=0 is genuinely "unchanged"; prev=0 & cur>0 has no + // meaningful percent — omit the point (gap) instead of fabricating 0%. + if (previousValue === 0) { + if (currentValue === 0) row[deltaSeriesName] = 0 + continue + } + row[deltaSeriesName] = ((currentValue - previousValue) / Math.abs(previousValue)) * 100 + } + } +} diff --git a/packages/query-engine/src/runtime/query-engine.ts b/packages/query-engine/src/runtime/query-engine.ts index b1ae290fa..883c7489a 100644 --- a/packages/query-engine/src/runtime/query-engine.ts +++ b/packages/query-engine/src/runtime/query-engine.ts @@ -36,6 +36,7 @@ import { formatWarehouseDateTime, parseWarehouseDateTime, } from "../datetime" +import { ENGINE_UNGROUPED_GROUP_KEY } from "../group-key" import { MAX_BREAKDOWN_RANGE_SECONDS, MAX_LIST_RANGE_SECONDS, @@ -216,17 +217,10 @@ export const msToTinybirdDateTime = (ms: number): string => { const CACHE_SNAP_S = 15 const TRACE_SERVICE_PARTITION_BUFFER_MS = 24 * 60 * 60 * 1000 -/** - * The engine's name for "this result has no grouping dimension". - * - * Storage, the wire and the UI spell the same thing `UNGROUPED_GROUP_KEY` - * (`"__total__"`), and `toStorageGroupKey` in the alerts service is the one - * boundary that translates between them. That translation was already named; - * this side was a bare `"all"` repeated at a dozen sites, which is what made it - * possible to write one of them as `"__total__"` by mistake and produce an - * `alert_rule_states` row no reader can see. - */ -export const ENGINE_UNGROUPED_GROUP_KEY = "all" +// Re-exported so `@maple/query-engine/runtime` consumers keep one import site; +// the definition is in the driver-free `../group-key` because the query-set merge +// needs it too and runs in the browser. +export { ENGINE_UNGROUPED_GROUP_KEY } from "../group-key" /** * Bound the service-enrichment lookup to the daily partitions surrounding the From 6e33d65061fbb935ec8eda9798e60ca005f374ba Mon Sep 17 00:00:00 2001 From: Makisuo Date: Thu, 13 Aug 2026 23:19:12 +0200 Subject: [PATCH 10/13] refactor(query-engine): run a query set through a shared runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The web app and the MCP widget inspector each had their own copy of "lower every draft, execute it, merge, evaluate formulas, shift a comparison window". This adds the layer both can stand on: a `QuerySetExecutor` port plus `runQuerySetWindow` (fan-out over one window) and `runTimeseriesQuerySet` (everything a chart needs from a stored QuerySet). The port is the whole design. Its shape is forced by having two hosts that share nothing else: no tenant parameter, because the browser has none and the API closes over one; `R = never`, because both discharge their requirements at construction; `E` generic, because neither should be mapped into a common error type it then has to map back out of; `describeError` on the port, because rendering a failure is a host concern. It returns the whole QueryEngineResult union so the runner can fold a wrong-shaped answer into one query's error instead of failing the set. Concurrency stays at the query count and is commented as load-bearing: the web executor is backed by a batcher that coalesces everything enqueued in one tick into a single POST /execute-batch, so bounding it here would quietly turn one round trip into several. Diagnostics are now always returned rather than gated behind `debug: true`. They were already computed unconditionally and then discarded, and this runs in the browser, so there is no wire cost — and the lab reads them typed instead of casting an `unknown` to guess at `queries[].fallbackUsed`. query-builder-timeseries.ts 547 -> 164, and the tests that were reachable only through a server function now run against an in-memory executor: hidden queries feeding formulas before being dropped, the un-widened comparison window, a failing fallback window not sinking a working chart. --- .../query-builder-timeseries.test.ts | 160 +----- .../api/warehouse/query-builder-timeseries.ts | 537 +++--------------- .../query-builder/query-builder-lab.tsx | 26 +- packages/query-engine/src/query-set/errors.ts | 42 ++ packages/query-engine/src/query-set/index.ts | 4 + packages/query-engine/src/query-set/port.ts | 46 ++ .../src/query-set/timeseries.test.ts | 394 +++++++++++++ .../query-engine/src/query-set/timeseries.ts | 208 +++++++ packages/query-engine/src/query-set/window.ts | 265 +++++++++ 9 files changed, 1050 insertions(+), 632 deletions(-) create mode 100644 packages/query-engine/src/query-set/errors.ts create mode 100644 packages/query-engine/src/query-set/port.ts create mode 100644 packages/query-engine/src/query-set/timeseries.test.ts create mode 100644 packages/query-engine/src/query-set/timeseries.ts create mode 100644 packages/query-engine/src/query-set/window.ts diff --git a/apps/web/src/api/warehouse/query-builder-timeseries.test.ts b/apps/web/src/api/warehouse/query-builder-timeseries.test.ts index ac5ab17f6..5987fcb19 100644 --- a/apps/web/src/api/warehouse/query-builder-timeseries.test.ts +++ b/apps/web/src/api/warehouse/query-builder-timeseries.test.ts @@ -1,29 +1,11 @@ -import { assert, describe, expect, it } from "@effect/vitest" -import { Effect } from "effect" -import type { QuerySpec } from "@maple/query-engine" -import { LAB_EMPTY_RANGE_STRATEGY, NO_EMPTY_RANGE_FALLBACK } from "@maple/query-engine/query-set" +import { describe, expect, it } from "vitest" +import { LAB_EMPTY_RANGE_STRATEGY } from "@maple/query-engine/query-set" import { __testables } from "@/api/warehouse/query-builder-timeseries" -import { WarehouseQueryError } from "@/api/warehouse/effect-utils" -import type { QueryRunResult } from "@/components/query-builder/formula-results" -// The pure shaping this module used to own — bucket sizing, execution windows, -// the series merge, percent change, hidden-id collection — now lives in -// `@maple/query-engine/query-set` and is tested there against no HTTP at all. -// What is left here is this module's own: mapping the wire strategy shape, -// driving the executor through the fallback ladder, and the no-data message. - -function makeQueryResult(overrides: Partial = {}): QueryRunResult { - return { - queryId: "q-1", - queryName: "A", - source: "traces", - status: "success", - error: null, - warnings: [], - data: [], - ...overrides, - } -} +// This module is now an adapter: the fan-out, fallback ladder, merge, comparison +// window and no-data diagnosis all live in `@maple/query-engine/query-set` and +// are tested there against an in-memory executor. What is still this module's own +// is translating the wire strategy shape, which `use-widget-data` depends on. describe("resolveStrategy (wire shape → package shape)", () => { it("maps the wire field names onto the strategy the package takes", () => { @@ -67,133 +49,3 @@ describe("resolveStrategy (wire shape → package shape)", () => { ).toEqual(LAB_EMPTY_RANGE_STRATEGY) }) }) - -describe("executeTimeseriesQueryWithFallbackUsing", () => { - it.effect("continues fallback execution after an error and recomputes window buckets", () => - Effect.gen(function* () { - const spec: QuerySpec = { - kind: "timeseries", - source: "traces", - metric: "count", - } - - const seenBucketSeconds: number[] = [] - const result = yield* __testables.executeTimeseriesQueryWithFallbackUsing( - "2026-01-02 00:00:00", - "2026-01-02 01:00:00", - spec, - { - enabled: true, - windowSeconds: [24 * 60 * 60, 7 * 24 * 60 * 60], - maxRangeSeconds: 31 * 24 * 60 * 60, - }, - true, - (windowStart, _windowEnd, windowSpec) => - Effect.gen(function* () { - if (windowSpec.kind !== "timeseries") { - return [] - } - - seenBucketSeconds.push(windowSpec.bucketSeconds ?? -1) - - if (windowStart === "2026-01-02 00:00:00") { - return [] - } - - if (windowStart === "2026-01-01 01:00:00") { - return yield* new WarehouseQueryError({ - operation: "test", - message: "Timeseries query too expensive", - }) - } - - return [ - { - bucket: "2026-01-01T00:00:00.000Z", - series: { total: 5 }, - }, - ] - }), - ) - - assert.deepStrictEqual(seenBucketSeconds, [60, 900, 3600]) - assert.isTrue(result.fallbackUsed) - assert.lengthOf(result.attempts, 3) - assert.strictEqual(result.attempts[1]?.error, "Maple could not complete the warehouse query.") - assert.deepStrictEqual(result.points, [ - { - bucket: "2026-01-01T00:00:00.000Z", - series: { total: 5 }, - }, - ]) - }), - ) - - it.effect("fails outright when the PRIMARY window errors — that is not a widening case", () => - Effect.gen(function* () { - const outcome = yield* Effect.result( - __testables.executeTimeseriesQueryWithFallbackUsing( - "2026-01-02 00:00:00", - "2026-01-02 01:00:00", - { kind: "timeseries", source: "traces", metric: "count" }, - LAB_EMPTY_RANGE_STRATEGY, - true, - () => - Effect.gen(function* () { - return yield* new WarehouseQueryError({ operation: "test", message: "boom" }) - }), - ), - ) - - assert.isTrue(outcome._tag === "Failure") - }), - ) - - it.effect("runs exactly one window when the strategy is off", () => - Effect.gen(function* () { - let calls = 0 - const result = yield* __testables.executeTimeseriesQueryWithFallbackUsing( - "2026-01-02 00:00:00", - "2026-01-02 01:00:00", - { kind: "timeseries", source: "traces", metric: "count" }, - NO_EMPTY_RANGE_FALLBACK, - true, - () => - Effect.sync(() => { - calls += 1 - return [] - }), - ) - - assert.strictEqual(calls, 1) - assert.isFalse(result.fallbackUsed) - }), - ) -}) - -describe("noQueryDataMessage", () => { - it("prefers query error message when no series data exists", () => { - const message = __testables.noQueryDataMessage([ - makeQueryResult({ - status: "error", - error: "Timeseries query too expensive", - }), - makeQueryResult({ - queryId: "q-2", - queryName: "B", - data: [], - }), - ]) - - expect(message).toContain("too expensive") - }) -}) - -describe("query-builder timeseries units", () => { - it("does not rescale error_rate series — the engine's 0–1 ratio is canonical", () => { - // Regression guard: a ÷100 "normalize" survived from the Tinybird-pipe - // era (which returned percent points) long after the CH engine switched - // to emitting ratios, making every error_rate chart 100× too small. - expect(__testables).not.toHaveProperty("normalizeErrorRatePoints") - }) -}) diff --git a/apps/web/src/api/warehouse/query-builder-timeseries.ts b/apps/web/src/api/warehouse/query-builder-timeseries.ts index 4beddf9e7..8b03c92cb 100644 --- a/apps/web/src/api/warehouse/query-builder-timeseries.ts +++ b/apps/web/src/api/warehouse/query-builder-timeseries.ts @@ -1,39 +1,14 @@ -import { Effect, Result, Schema } from "effect" -import { - formatWarehouseDateTime, - parseWarehouseDateTime, - QueryEngineExecuteRequest, - type QuerySpec, -} from "@maple/query-engine" -import { NO_QUERY_DATA_MESSAGE } from "@/lib/alerts/preview-failure" -import { - buildFormulaResults, - type FormulaDraft, - type QueryRunResult, - type TimeseriesPoint, -} from "@/components/query-builder/formula-results" +import { Effect, Schema } from "effect" +import { QueryEngineExecuteRequest } from "@maple/query-engine" import { QueryBuilderQueryDraftSchema } from "@maple/domain/http" +import { QueryBuilderFormulaSchema, QueryComparisonSchema } from "@maple/query-model" import { - QueryBuilderFormulaSchema, - type QueryComparisonMode, - QueryComparisonSchema, -} from "@maple/query-model" -import { buildTimeseriesQuerySpec } from "@maple/query-engine/query-builder" -import { - appendPercentChangeSeries, - buildExecutionWindows, - collectHiddenResultIds, - combineRows, - countSuccessfulQuerySeries, - type EmptyRangeFallbackStrategy, - hasAnySeriesData, LAB_EMPTY_RANGE_STRATEGY, - mergeQueryRunResults, - resolveExecutionSpecForWindow, + type EmptyRangeFallbackStrategy, + type QuerySetExecutor, + type TimeseriesQuerySetDiagnostics, resolveFallbackStrategy, - resolveTimeseriesBucketSpec, - shiftRunResults, - toDisplayNameById, + runTimeseriesQuerySet, } from "@maple/query-engine/query-set" import { decodeInput, @@ -44,6 +19,20 @@ import { } from "@/api/warehouse/effect-utils" import { displayError } from "@/lib/error-messages" +/** + * The browser-side adapter for `runTimeseriesQuerySet`. + * + * Everything that shapes a query set into chart rows lives in + * `@maple/query-engine/query-set`, shared with the MCP widget inspector. What is + * left here is the three things that are genuinely this app's: + * + * 1. decoding the wire input, + * 2. building a `QuerySetExecutor` from the HTTP batcher, + * 3. mapping the runner's tagged failures back onto `WarehouseInvalidInputError` + * with byte-identical messages, because `mapBuilderChartFailure` in the + * alert-preview path still string-matches them. + */ + type ExecuteError = WarehouseApiError | BackendError const dateTimeString = Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)) @@ -63,63 +52,20 @@ const QueryBuilderTimeseriesInputSchema = Schema.Struct({ formulas: Schema.optional(Schema.mutable(Schema.Array(QueryBuilderFormulaSchema))), comparison: Schema.optional(QueryComparisonSchema), strategy: Schema.optional(StrategySchema), - debug: Schema.optional(Schema.Boolean), }) export type QueryBuilderTimeseriesInput = Schema.Schema.Type -interface QueryExecutionAttempt { - startTime: string - endTime: string - kind: "primary" | "fallback" - points: number - hasSeries: boolean - error?: string -} - -interface QueryExecutionDebug { - queryId: string - queryName: string - source: string - spec: QuerySpec | null - attempts: QueryExecutionAttempt[] - fallbackUsed: boolean -} - -interface QueryBuilderTimeseriesDebug { - primaryWindow: { - startTime: string - endTime: string - } - comparison: { - mode: QueryComparisonMode - includePercentChange: boolean - shiftedByMs: number - previousStartTime: string | null - previousEndTime: string | null - } - strategy: { - enableEmptyRangeFallback: boolean - fallbackWindowSeconds: number[] - maxFallbackRangeSeconds: number - } - queries: QueryExecutionDebug[] - previousQueries: QueryExecutionDebug[] -} - interface QueryBuilderTimeseriesResponse { data: Array> - debug?: QueryBuilderTimeseriesDebug -} - -const toEpochMs = parseWarehouseDateTime - -function noQueryDataMessage(queryResults: QueryRunResult[]): string { - const firstQueryError = queryResults.find( - (result) => typeof result.error === "string" && result.error.length > 0, - )?.error - - return firstQueryError ?? NO_QUERY_DATA_MESSAGE + /** + * Always present, never behind a `debug` flag. + * + * It was already computed unconditionally and then discarded unless the caller + * asked for it, and this function runs in the browser — so there is no wire + * cost to returning it, and the lab reads it typed instead of casting. + */ + diagnostics: TimeseriesQuerySetDiagnostics } /** @@ -146,241 +92,25 @@ function resolveStrategy(input: QueryBuilderTimeseriesInput): EmptyRangeFallback ) } -const executeTimeseriesQuery = Effect.fn("QueryEngine.executeTimeseriesQuery")(function* ( - startTime: string, - endTime: string, - spec: QuerySpec, -) { - const request = yield* decodeInput( - QueryEngineExecuteRequest, - { startTime, endTime, query: spec }, - "executeTimeseriesQuery.request", - ) - - const response = yield* executeQueryEngine("queryEngine.timeseriesQuery", request) - - if (response.result.kind !== "timeseries") { - return yield* invalidWarehouseInput("executeTimeseriesQuery", "Unexpected non-timeseries result") - } - - return response.result.data.map((point) => ({ - bucket: point.bucket, - series: { ...point.series }, - })) satisfies TimeseriesPoint[] -}) - -type ExecuteTimeseriesFn = ( - startTime: string, - endTime: string, - spec: QuerySpec, -) => Effect.Effect - -function executeTimeseriesQueryWithFallback( - startTime: string, - endTime: string, - spec: QuerySpec, - strategy: ReturnType, - allowFallback: boolean, -) { - return executeTimeseriesQueryWithFallbackUsing( - startTime, - endTime, - spec, - strategy, - allowFallback, - executeTimeseriesQuery, - ) -} - -const executeTimeseriesQueryWithFallbackUsing = Effect.fn("QueryEngine.executeTimeseriesQueryWithFallback")( - function* ( - startTime: string, - endTime: string, - spec: QuerySpec, - strategy: ReturnType, - allowFallback: boolean, - executeFn: ExecuteTimeseriesFn, - ) { - const windows = buildExecutionWindows(startTime, endTime, strategy, allowFallback) - const attempts: QueryExecutionAttempt[] = [] - let lastPoints: TimeseriesPoint[] = [] - - for (const [index, window] of windows.entries()) { - const windowSpec = resolveExecutionSpecForWindow(spec, window) - - const outcome = yield* Effect.result(executeFn(window.startTime, window.endTime, windowSpec)) - - if (Result.isFailure(outcome)) { - const error = outcome.failure - const message = displayError(error).message - - attempts.push({ - startTime: window.startTime, - endTime: window.endTime, - kind: window.kind, - points: 0, - hasSeries: false, - error: message, - }) - - if (window.kind === "primary") { - return yield* Effect.fail(error) - } - continue - } - - const points = outcome.success - const hasSeries = hasAnySeriesData(points) - - attempts.push({ - startTime: window.startTime, - endTime: window.endTime, - kind: window.kind, - points: points.length, - hasSeries, - }) - lastPoints = points - - if (hasSeries) { - return { - points, - attempts, - fallbackUsed: index > 0, - } - } - } - - return { - points: lastPoints, - attempts, - fallbackUsed: false, - } - }, -) - -const runQueryWindow = Effect.fn("QueryEngine.runQueryWindow")(function* ( - startTime: string, - endTime: string, - enabledQueries: QueryBuilderTimeseriesInput["queries"], - formulas: FormulaDraft[], - strategy: ReturnType, - allowFallback: boolean, -) { - const debug: QueryExecutionDebug[] = [] - - const queryResults = yield* Effect.forEach( - enabledQueries, - (query) => - Effect.gen(function* () { - const built = buildTimeseriesQuerySpec(query) - - if (!built.query) { - debug.push({ - queryId: query.id, - queryName: query.name, - source: query.dataSource, - spec: null, - attempts: [], - fallbackUsed: false, - }) - - return { - queryId: query.id, - queryName: query.name, - source: query.dataSource, - status: "error", - error: built.error ?? "Failed to build query", - warnings: built.warnings, - data: [], - } satisfies QueryRunResult - } - - const querySpec = resolveTimeseriesBucketSpec(built.query, startTime, endTime) - - const outcome = yield* Effect.result( - executeTimeseriesQueryWithFallback( - startTime, - endTime, - querySpec, - strategy, - allowFallback, - ), - ) - - if (Result.isFailure(outcome)) { - const error = outcome.failure - debug.push({ - queryId: query.id, - queryName: query.name, - source: query.dataSource, - spec: querySpec, - attempts: [], - fallbackUsed: false, - }) - - return { - queryId: query.id, - queryName: query.name, - source: query.dataSource, - status: "error", - error: displayError(error).message, - warnings: built.warnings, - data: [], - } satisfies QueryRunResult - } - - const execution = outcome.success - debug.push({ - queryId: query.id, - queryName: query.name, - source: query.dataSource, - spec: querySpec, - attempts: execution.attempts, - fallbackUsed: execution.fallbackUsed, - }) - - const warnings = [...built.warnings] - if (execution.fallbackUsed) { - const selectedAttempt = execution.attempts[execution.attempts.length - 1] - warnings.push( - `No data in requested range; used fallback window ${selectedAttempt.startTime} -> ${selectedAttempt.endTime}`, - ) - } - - return { - queryId: query.id, - queryName: query.name, - source: query.dataSource, - status: "success", - error: null, - warnings, - // error_rate arrives from the query engine as a 0–1 ratio — the - // canonical unit everywhere (the "percent" display unit multiplies - // by 100 when formatting). No rescaling here. - data: execution.points, - } satisfies QueryRunResult - }), - { concurrency: enabledQueries.length }, - ) - - const formulaResults = - countSuccessfulQuerySeries(queryResults) > 0 ? buildFormulaResults(formulas, queryResults) : [] - return { - queryResults, - allResults: [...queryResults, ...formulaResults], - debug, - } -}) - -// The pure shaping — bucket sizing, execution windows, the series merge, percent -// change, hidden-id collection — moved to `@maple/query-engine/query-set` and is -// tested there. What remains here is this module's own: the wire strategy -// mapping, the fallback loop that drives the HTTP executor, and the "why is -// there no data" message. -export const __testables = { - resolveStrategy, - executeTimeseriesQueryWithFallbackUsing, - noQueryDataMessage, +/** + * The port, backed by the HTTP batcher. + * + * `executeQueryEngine` enqueues onto a per-tick batcher that coalesces every + * request made in the same tick into one `POST /execute-batch`. That is why the + * runner fans out at full concurrency rather than bounding itself. + */ +const warehouseExecutor: QuerySetExecutor = { + execute: (request) => + Effect.gen(function* () { + const decoded = yield* decodeInput( + QueryEngineExecuteRequest, + { startTime: request.startTime, endTime: request.endTime, query: request.query }, + "executeTimeseriesQuery.request", + ) + const response = yield* executeQueryEngine("queryEngine.timeseriesQuery", decoded) + return response.result + }), + describeError: (error) => displayError(error).message, } export function getQueryBuilderTimeseries({ data }: { data: QueryBuilderTimeseriesInput }) { @@ -393,153 +123,42 @@ const getQueryBuilderTimeseriesEffect = Effect.fn("QueryEngine.getQueryBuilderTi data: QueryBuilderTimeseriesInput }) { const input = yield* decodeInput(QueryBuilderTimeseriesInputSchema, data, "getQueryBuilderTimeseries") - - const formulas: FormulaDraft[] = (input.formulas ?? []).map((formula) => ({ - id: formula.id, - name: formula.name, - expression: formula.expression, - legend: formula.legend, - })) - const hiddenResultIds = collectHiddenResultIds(input) - const isPlotted = (result: QueryRunResult): boolean => !hiddenResultIds.has(result.queryId) const strategy = resolveStrategy(input) - const comparison = { - mode: input.comparison?.mode ?? "none", - includePercentChange: input.comparison?.includePercentChange ?? true, - } as const - const enabledQueries = input.queries.filter((query) => query.enabled !== false) - if (enabledQueries.length === 0) { - return yield* invalidWarehouseInput("getQueryBuilderTimeseries", "No enabled queries to run") - } - - const currentWindow = yield* runQueryWindow( - input.startTime, - input.endTime, - enabledQueries, - formulas, - strategy, - true, + const outcome = yield* runTimeseriesQuerySet(warehouseExecutor, { + querySet: { + queries: input.queries, + ...(input.formulas === undefined ? {} : { formulas: input.formulas }), + ...(input.comparison === undefined ? {} : { comparison: input.comparison }), + }, + startTime: input.startTime, + endTime: input.endTime, + fallback: strategy, + }).pipe( + // The runner's tagged failures carry the message this app already showed; + // re-raising them as `WarehouseInvalidInputError` keeps `displayError` and + // `mapBuilderChartFailure` working unchanged. + Effect.catchTags({ + "@maple/query-engine/query-set/QuerySetInputError": (error) => + invalidWarehouseInput("getQueryBuilderTimeseries", error.message), + "@maple/query-engine/query-set/QuerySetNoDataError": (error) => + invalidWarehouseInput("getQueryBuilderTimeseries", error.message), + }), ) - const successfulQueryCount = countSuccessfulQuerySeries(currentWindow.queryResults) - if (successfulQueryCount === 0) { - return yield* invalidWarehouseInput( - "getQueryBuilderTimeseries", - noQueryDataMessage(currentWindow.queryResults), - ) - } - - const allResults = currentWindow.allResults - - const successfulCount = allResults.filter( - (result) => result.status === "success" && hasAnySeriesData(result.data), - ).length - - if (successfulCount === 0) { - const firstError = allResults.find((result) => result.error)?.error - return yield* invalidWarehouseInput( - "getQueryBuilderTimeseries", - firstError ?? "No successful query results", - ) - } - // Data came back, but nothing plottable did — say why rather than drawing an empty chart the - // reader would blame on the time range. On a ratio widget the plotted series is the formula, - // so its own failure (an unknown reference, no overlapping buckets) is the useful message. - if (!allResults.some((result) => isPlotted(result) && hasAnySeriesData(result.data))) { - const plottedError = allResults.find((result) => isPlotted(result) && result.error)?.error - return yield* invalidWarehouseInput( - "getQueryBuilderTimeseries", - plottedError ?? "Every query and formula with data is hidden — nothing to plot", - ) - } - - const displayNameById = toDisplayNameById([ - ...enabledQueries, - ...formulas.map((formula) => ({ - id: formula.id, - name: formula.name, - legend: formula.legend, - })), - ]) - const usedSeriesNames = new Set() - const mergedCurrent = mergeQueryRunResults(allResults.filter(isPlotted), displayNameById, { - usedSeriesNames, + yield* Effect.annotateCurrentSpan({ + "query_set.query_count": input.queries.length, + "query_set.formula_count": input.formulas?.length ?? 0, + "query_set.comparison_mode": outcome.diagnostics.comparison.mode, + "query_set.fallback_used": outcome.diagnostics.queries.some((query) => query.fallbackUsed), }) - const mergedSets = [mergedCurrent] - let mergedPrevious: { - rowsByBucket: Map> - seriesNameByStableKey: Map - seriesNames: string[] - } | null = null - - const startMs = toEpochMs(input.startTime) - const endMs = toEpochMs(input.endTime) - const shiftMs = Number.isFinite(startMs) && Number.isFinite(endMs) ? endMs - startMs : 0 - let previousStartTime: string | null = null - let previousEndTime: string | null = null - let previousDebug: QueryExecutionDebug[] = [] - - if (comparison.mode === "previous_period" && shiftMs > 0) { - previousStartTime = formatWarehouseDateTime(startMs - shiftMs) - previousEndTime = formatWarehouseDateTime(endMs - shiftMs) - - const previousWindow = yield* runQueryWindow( - previousStartTime, - previousEndTime, - enabledQueries, - formulas, - strategy, - false, - ) - previousDebug = previousWindow.debug - - const shiftedPreviousResults = shiftRunResults(previousWindow.allResults.filter(isPlotted), shiftMs) - mergedPrevious = mergeQueryRunResults(shiftedPreviousResults, displayNameById, { - seriesSuffix: " (prev)", - usedSeriesNames, - }) - mergedSets.push(mergedPrevious) - } - - const mergedRows = combineRows(mergedSets) - if (comparison.mode === "previous_period" && comparison.includePercentChange && mergedPrevious) { - appendPercentChangeSeries( - mergedRows, - mergedCurrent.seriesNameByStableKey, - mergedPrevious.seriesNameByStableKey, - ) - } - - const debugInfo: QueryBuilderTimeseriesDebug = { - primaryWindow: { - startTime: input.startTime, - endTime: input.endTime, - }, - comparison: { - mode: comparison.mode, - includePercentChange: comparison.includePercentChange, - shiftedByMs: shiftMs > 0 ? shiftMs : 0, - previousStartTime, - previousEndTime, - }, - // Reported under the wire spelling, which is what the caller sent and what - // the lab's debug panel labels its rows with. - strategy: { - enableEmptyRangeFallback: strategy.enabled, - fallbackWindowSeconds: [...strategy.windowSeconds], - maxFallbackRangeSeconds: strategy.maxRangeSeconds, - }, - queries: currentWindow.debug, - previousQueries: previousDebug, - } - - if (input.debug === true) { - yield* Effect.logInfo("timeseries execution", debugInfo) - } return { - data: mergedRows, - ...(input.debug === true ? { debug: debugInfo } : {}), + data: [...outcome.rows], + diagnostics: outcome.diagnostics, } satisfies QueryBuilderTimeseriesResponse }) + +export const __testables = { + resolveStrategy, +} diff --git a/apps/web/src/components/query-builder/query-builder-lab.tsx b/apps/web/src/components/query-builder/query-builder-lab.tsx index 0ec552863..ea684f9af 100644 --- a/apps/web/src/components/query-builder/query-builder-lab.tsx +++ b/apps/web/src/components/query-builder/query-builder-lab.tsx @@ -36,6 +36,7 @@ import { } from "@/lib/services/atoms/warehouse-query-atoms" import { type FormulaDraft, type TimeseriesPoint } from "@/components/query-builder/formula-results" import { type QueryBuilderTimeseriesInput } from "@/api/warehouse/query-builder-timeseries" +import { type TimeseriesQuerySetDiagnostics } from "@maple/query-engine/query-set" import { AGGREGATIONS_BY_SOURCE, createFormulaDraft, @@ -138,23 +139,10 @@ function toRunPoints(rows: Array>): TimeseriesPo }) } -function debugWarnings(debug: unknown): string[] { - if (!debug || typeof debug !== "object") { - return [] - } - - const debugObj = debug as { - queries?: Array<{ queryName?: string; fallbackUsed?: boolean }> - } - - const warnings: string[] = [] - for (const entry of debugObj.queries ?? []) { - if (entry.fallbackUsed) { - warnings.push(`${entry.queryName ?? "query"} used fallback range`) - } - } - - return warnings +function debugWarnings(diagnostics: TimeseriesQuerySetDiagnostics): string[] { + return diagnostics.queries + .filter((entry) => entry.fallbackUsed) + .map((entry) => `${entry.queryName} used fallback range`) } const GROUP_BY_OPTIONS: Record> = { @@ -252,7 +240,7 @@ function QueryBuilderAtomResults({ input }: { input: QueryBuilderTimeseriesInput )) .onSuccess((response) => { const data = toRunPoints(response.data) - const warnings = debugWarnings(response.debug) + const warnings = debugWarnings(response.diagnostics) const seriesKeys = Array.from( new Set(data.flatMap((point) => Object.keys(point.series))), ).slice(0, 6) @@ -471,7 +459,7 @@ function QueryBuilderLabInner({ startTime, endTime }: QueryBuilderLabProps) { } setNoQueriesError(null) - setSubmittedInput({ startTime, endTime, queries, formulas, debug: true }) + setSubmittedInput({ startTime, endTime, queries, formulas }) setLastRunAt(new Date().toLocaleTimeString()) }, [endTime, formulas, queries, startTime]) diff --git a/packages/query-engine/src/query-set/errors.ts b/packages/query-engine/src/query-set/errors.ts new file mode 100644 index 000000000..7f87979f0 --- /dev/null +++ b/packages/query-engine/src/query-set/errors.ts @@ -0,0 +1,42 @@ +import { Schema } from "effect" + +/** + * The message a caller shows when every query ran but none returned rows. + * + * Exported as a constant because the alert-preview path string-matches it to + * tell "your range is empty" (a muted, expected state) apart from "your query is + * broken" (a red error card). Changing the text without changing that match + * would turn every empty preview back into an error. + */ +export const NO_QUERY_DATA_MESSAGE = "No query data found in selected time range" + +/** + * The query set cannot produce a result as described — nothing to execute, or + * nothing left to draw once hidden series are removed. + * + * Distinct from a warehouse failure: the input is the problem, and retrying it + * unchanged will fail the same way. + */ +export class QuerySetInputError extends Schema.TaggedError()( + "@maple/query-engine/query-set/QuerySetInputError", + { + operation: Schema.String, + message: Schema.String, + }, +) {} + +/** + * Every query ran and none of them returned data for the window. + * + * Its own tag rather than a flavour of `QuerySetInputError` because it is not a + * fault — an empty window is a normal answer — and callers render it as a muted + * "No data" state rather than an error. + */ +export class QuerySetNoDataError extends Schema.TaggedError()( + "@maple/query-engine/query-set/QuerySetNoDataError", + { + message: Schema.String, + /** Per-query messages, when a query failed rather than simply returning nothing. */ + details: Schema.Array(Schema.String), + }, +) {} diff --git a/packages/query-engine/src/query-set/index.ts b/packages/query-engine/src/query-set/index.ts index 575a056e3..ea25434fd 100644 --- a/packages/query-engine/src/query-set/index.ts +++ b/packages/query-engine/src/query-set/index.ts @@ -22,4 +22,8 @@ export * from "./breakdown-merge" export * from "./bucketing" +export * from "./errors" +export * from "./port" export * from "./series-merge" +export * from "./timeseries" +export * from "./window" diff --git a/packages/query-engine/src/query-set/port.ts b/packages/query-engine/src/query-set/port.ts new file mode 100644 index 000000000..69315fe3c --- /dev/null +++ b/packages/query-engine/src/query-set/port.ts @@ -0,0 +1,46 @@ +import type { Effect } from "effect" +import type { QueryEngineResult, QuerySpec } from "@maple/domain/query-engine" + +export interface QuerySetExecuteRequest { + readonly startTime: string + readonly endTime: string + readonly query: QuerySpec +} + +/** + * "Execute one QuerySpec over one window" — the only thing the runner needs from + * a host, and the only thing its two hosts genuinely share. + * + * The web app satisfies it with `executeQueryEngine` (an HTTP call that a + * per-tick batcher coalesces into one `POST /execute-batch`); the API satisfies + * it with `QueryEngineService.execute`, straight to the warehouse. + * + * Every part of this shape is load-bearing: + * + * - **No tenant parameter.** The web app has no tenant to pass — it is the JWT + * — while the API adapter closes over `TenantContext`. A tenant parameter + * would force the web side to invent one. + * - **`R = never`.** The web executor self-provides through its runtime, and + * the API adapter is built inside a generator that has already yielded + * `QueryEngineService`, so the requirement is discharged at construction. An + * `R` type parameter would infect every runner signature to buy nothing. + * - **`E` stays generic.** The runner never inspects it; each host keeps its + * own error union rather than being mapped into a lowest common denominator + * that both then have to map back out of. + * - **Returns the whole `QueryEngineResult` union**, not a narrowed arm. The + * runner asserts `result.kind` itself and folds a mismatch into a per-query + * error, which is what keeps one wrong-shaped query from failing the batch. + * - **One method, no batch method.** Batching, caching and HTTP-vs-direct are + * adapter policy. What the runner owes the adapter in return is issuing its + * per-query calls in ONE tick — see the concurrency note on + * `runQuerySetWindow`. + */ +export interface QuerySetExecutor { + readonly execute: (request: QuerySetExecuteRequest) => Effect.Effect + /** + * Render a host failure as the per-query message a chart shows on its failed + * series card. On the web that is `displayError`; on the API it is the + * failure's own message. Neither belongs in this package. + */ + readonly describeError: (error: E) => string +} diff --git a/packages/query-engine/src/query-set/timeseries.test.ts b/packages/query-engine/src/query-set/timeseries.test.ts new file mode 100644 index 000000000..30c9298a4 --- /dev/null +++ b/packages/query-engine/src/query-set/timeseries.test.ts @@ -0,0 +1,394 @@ +import { assert, describe, it } from "@effect/vitest" +import { Effect } from "effect" +import type { QueryEngineResult } from "@maple/domain/query-engine" +import type { QueryBuilderQueryDraftPayload, QuerySet } from "@maple/query-model" +import { LAB_EMPTY_RANGE_STRATEGY } from "./bucketing" +import type { QuerySetExecutor, QuerySetExecuteRequest } from "./port" +import { runTimeseriesQuerySet } from "./timeseries" +import { runQuerySetWindow } from "./window" + +/** + * The runner is defined entirely by what it asks a `QuerySetExecutor` for and + * what it does with the answers, so these tests drive it with an in-memory one. + * No HTTP, no warehouse — which is the point of the port existing. + */ +class FakeError { + constructor(readonly message: string) {} +} + +type FakeResponse = + | { readonly _tag: "ok"; readonly result: QueryEngineResult } + | { readonly _tag: "fail"; readonly error: FakeError } + +/** A successful timeseries answer carrying these points. */ +const ok = (points: ReadonlyArray<{ bucket: string; series: Record }>): FakeResponse => ({ + _tag: "ok", + result: { kind: "timeseries", source: "traces", data: points }, +}) + +/** Any other result shape — the "host answered a different question" case. */ +const raw = (result: QueryEngineResult): FakeResponse => ({ _tag: "ok", result }) + +const fail = (message: string): FakeResponse => ({ _tag: "fail", error: new FakeError(message) }) + +const makeExecutor = (respond: (request: QuerySetExecuteRequest) => FakeResponse) => { + const requests: QuerySetExecuteRequest[] = [] + const executor: QuerySetExecutor = { + execute: (request) => + Effect.suspend((): Effect.Effect => { + requests.push(request) + const response = respond(request) + return response._tag === "fail" + ? Effect.fail(response.error) + : Effect.succeed(response.result) + }), + describeError: (error) => error.message, + } + return { executor, requests } +} + +const draft = (overrides: Partial = {}): QueryBuilderQueryDraftPayload => + ({ + id: "q-1", + name: "A", + dataSource: "traces", + aggregation: "count", + ...overrides, + }) as QueryBuilderQueryDraftPayload + +const set = (overrides: Partial = {}): QuerySet => ({ + queries: [draft()], + ...overrides, +}) + +const WINDOW = { startTime: "2026-01-02 00:00:00", endTime: "2026-01-02 01:00:00" } + +const point = (bucket: string, series: Record) => ({ bucket, series }) + +const isLogs = (request: QuerySetExecuteRequest) => + request.query.kind === "timeseries" && request.query.source === "logs" + +describe("runQuerySetWindow", () => { + it.effect("issues one execute per query and folds a build failure without executing it", () => + Effect.gen(function* () { + const { executor, requests } = makeExecutor(() => + ok([point("2026-01-02T00:00:00.000Z", { all: 1 })]), + ) + + const result = yield* runQuerySetWindow(executor, { + queries: [ + draft({ id: "ok", name: "OK" }), + // An empty aggregation cannot be lowered, so this never reaches the + // executor — it becomes an error result instead. + draft({ id: "bad", name: "Bad", aggregation: "" }), + ], + formulas: [], + ...WINDOW, + }) + + assert.strictEqual(requests.length, 1) + assert.strictEqual(result.queryResults.length, 2) + assert.strictEqual(result.queryResults[0].status, "success") + assert.strictEqual(result.queryResults[1].status, "error") + // A build failure records a diagnostic with no spec, which is how a debug + // panel tells "never ran" apart from "ran and failed". + assert.isNull(result.diagnostics.find((d) => d.queryId === "bad")?.spec ?? null) + }), + ) + + it.effect("folds a warehouse failure into that query's status, leaving siblings alone", () => + Effect.gen(function* () { + const { executor } = makeExecutor((request) => + isLogs(request) ? fail("logs are down") : ok([point("2026-01-02T00:00:00.000Z", { all: 3 })]), + ) + + const result = yield* runQuerySetWindow(executor, { + queries: [draft({ id: "t" }), draft({ id: "l", dataSource: "logs" })], + formulas: [], + ...WINDOW, + }) + + assert.strictEqual(result.queryResults[0].status, "success") + assert.strictEqual(result.queryResults[1].status, "error") + assert.strictEqual(result.queryResults[1].error, "logs are down") + }), + ) + + it.effect("treats a non-timeseries result as no data rather than crashing the set", () => + Effect.gen(function* () { + const { executor } = makeExecutor(() => raw({ kind: "breakdown", source: "traces", data: [] })) + + const result = yield* runQuerySetWindow(executor, { + queries: [draft()], + formulas: [], + ...WINDOW, + }) + + assert.strictEqual(result.queryResults[0].status, "success") + assert.deepStrictEqual(result.queryResults[0].data, []) + }), + ) + + it.effect("does not evaluate formulas when no query returned data", () => + Effect.gen(function* () { + const { executor } = makeExecutor(() => ok([])) + + const result = yield* runQuerySetWindow(executor, { + queries: [draft({ id: "q-1", name: "A" })], + formulas: [{ id: "f-1", name: "F", expression: "A * 2", legend: "" }], + ...WINDOW, + }) + + assert.lengthOf(result.formulaResults, 0) + }), + ) + + it.effect("does not widen by default — only when a strategy is passed", () => + Effect.gen(function* () { + const noFallback = makeExecutor(() => ok([])) + yield* runQuerySetWindow(noFallback.executor, { queries: [draft()], formulas: [], ...WINDOW }) + assert.strictEqual(noFallback.requests.length, 1) + + const withFallback = makeExecutor(() => ok([])) + yield* runQuerySetWindow(withFallback.executor, { + queries: [draft()], + formulas: [], + ...WINDOW, + fallback: LAB_EMPTY_RANGE_STRATEGY, + }) + // primary + 24h + 7d + 31d + assert.strictEqual(withFallback.requests.length, 4) + }), + ) + + it.effect("skips a failing fallback window rather than failing the query", () => + Effect.gen(function* () { + // The caller asked about the primary window; a speculative widening that + // is too expensive must not turn a working chart into an error. + const { executor } = makeExecutor((request) => + request.startTime === WINDOW.startTime + ? ok([]) + : request.startTime === "2026-01-01 01:00:00" + ? fail("too expensive") + : ok([point("2026-01-01T00:00:00.000Z", { all: 5 })]), + ) + + const result = yield* runQuerySetWindow(executor, { + queries: [draft()], + formulas: [], + ...WINDOW, + fallback: LAB_EMPTY_RANGE_STRATEGY, + }) + + assert.strictEqual(result.queryResults[0].status, "success") + assert.isTrue(result.diagnostics[0].fallbackUsed) + assert.include(result.queryResults[0].warnings.join(" "), "used fallback window") + }), + ) + + it.effect("stops at the primary window when IT fails — that is not a widening case", () => + Effect.gen(function* () { + const { executor, requests } = makeExecutor(() => fail("boom")) + + const result = yield* runQuerySetWindow(executor, { + queries: [draft()], + formulas: [], + ...WINDOW, + fallback: LAB_EMPTY_RANGE_STRATEGY, + }) + + assert.strictEqual(requests.length, 1) + assert.strictEqual(result.queryResults[0].status, "error") + assert.strictEqual(result.queryResults[0].error, "boom") + }), + ) +}) + +describe("runTimeseriesQuerySet", () => { + it.effect("merges a multi-query set into bucket rows", () => + Effect.gen(function* () { + const { executor } = makeExecutor((request) => + isLogs(request) + ? ok([point("2026-01-02T00:00:00.000Z", { all: 7 })]) + : ok([point("2026-01-02T00:00:00.000Z", { all: 3 })]), + ) + + const result = yield* runTimeseriesQuerySet(executor, { + querySet: set({ + queries: [ + draft({ id: "t", name: "Traces" }), + draft({ id: "l", name: "Logs", dataSource: "logs" }), + ], + }), + ...WINDOW, + }) + + assert.deepStrictEqual(result.rows, [{ bucket: "2026-01-02T00:00:00.000Z", Traces: 3, Logs: 7 }]) + }), + ) + + it.effect("skips disabled queries but still runs hidden ones, so formulas keep their operands", () => + Effect.gen(function* () { + const { executor, requests } = makeExecutor(() => + ok([point("2026-01-02T00:00:00.000Z", { all: 4 })]), + ) + + const result = yield* runTimeseriesQuerySet(executor, { + querySet: set({ + queries: [ + draft({ id: "a", name: "A", hidden: true }), + draft({ id: "b", name: "B", enabled: false }), + ], + formulas: [{ id: "f", name: "F", expression: "A * 2", legend: "Doubled" }], + }), + ...WINDOW, + }) + + // One execute: the disabled query never ran, the hidden one did. + assert.strictEqual(requests.length, 1) + // Only the formula is plotted — the hidden operand is dropped AFTER it fed + // the formula, not before. + assert.deepStrictEqual(result.rows, [{ bucket: "2026-01-02T00:00:00.000Z", Doubled: 8 }]) + }), + ) + + it.effect("fails with QuerySetInputError when the set has nothing enabled", () => + Effect.gen(function* () { + const { executor } = makeExecutor(() => ok([])) + const outcome = yield* Effect.result( + runTimeseriesQuerySet(executor, { + querySet: set({ queries: [draft({ enabled: false })] }), + ...WINDOW, + }), + ) + + assert.isTrue(outcome._tag === "Failure") + if (outcome._tag !== "Failure") return + assert.strictEqual(outcome.failure._tag, "@maple/query-engine/query-set/QuerySetInputError") + }), + ) + + it.effect("fails with QuerySetNoDataError when every query came back empty", () => + Effect.gen(function* () { + const { executor } = makeExecutor(() => ok([])) + const outcome = yield* Effect.result( + runTimeseriesQuerySet(executor, { querySet: set(), ...WINDOW }), + ) + + assert.isTrue(outcome._tag === "Failure") + if (outcome._tag !== "Failure") return + assert.strictEqual(outcome.failure._tag, "@maple/query-engine/query-set/QuerySetNoDataError") + }), + ) + + it.effect("reports the query's own error when the whole set failed", () => + Effect.gen(function* () { + const { executor } = makeExecutor(() => fail("Timeseries query too expensive")) + const outcome = yield* Effect.result( + runTimeseriesQuerySet(executor, { querySet: set(), ...WINDOW }), + ) + + assert.isTrue(outcome._tag === "Failure") + if (outcome._tag !== "Failure") return + assert.include(String(outcome.failure.message), "too expensive") + }), + ) + + it.effect("fails when data came back but every series with data is hidden", () => + Effect.gen(function* () { + const { executor } = makeExecutor(() => ok([point("2026-01-02T00:00:00.000Z", { all: 4 })])) + + const outcome = yield* Effect.result( + runTimeseriesQuerySet(executor, { + querySet: set({ queries: [draft({ hidden: true })] }), + ...WINDOW, + }), + ) + + assert.isTrue(outcome._tag === "Failure") + if (outcome._tag !== "Failure") return + assert.include(String(outcome.failure.message), "nothing to plot") + }), + ) + + describe("previous_period comparison", () => { + it.effect("runs a shifted window and appends (prev) and (%Δ) series", () => + Effect.gen(function* () { + const { executor, requests } = makeExecutor((request) => + request.startTime === WINDOW.startTime + ? ok([point("2026-01-02T00:00:00.000Z", { all: 20 })]) + : ok([point("2026-01-01T23:00:00.000Z", { all: 10 })]), + ) + + const result = yield* runTimeseriesQuerySet(executor, { + querySet: set({ + queries: [draft({ id: "q-1", name: "Requests" })], + comparison: { mode: "previous_period", includePercentChange: true }, + }), + ...WINDOW, + }) + + assert.deepStrictEqual( + requests.map((r) => r.startTime), + [WINDOW.startTime, "2026-01-01 23:00:00"], + ) + assert.deepStrictEqual(result.rows, [ + { + bucket: "2026-01-02T00:00:00.000Z", + Requests: 20, + "Requests (prev)": 10, + "Requests (%Δ)": 100, + }, + ]) + assert.strictEqual(result.diagnostics.comparison.previousStartTime, "2026-01-01 23:00:00") + }), + ) + + it.effect("omits the percent-change series when the caller opts out", () => + Effect.gen(function* () { + const { executor } = makeExecutor((request) => + request.startTime === WINDOW.startTime + ? ok([point("2026-01-02T00:00:00.000Z", { all: 20 })]) + : ok([point("2026-01-01T23:00:00.000Z", { all: 10 })]), + ) + + const result = yield* runTimeseriesQuerySet(executor, { + querySet: set({ + queries: [draft({ id: "q-1", name: "Requests" })], + comparison: { mode: "previous_period", includePercentChange: false }, + }), + ...WINDOW, + }) + + assert.notProperty(result.rows[0], "Requests (%Δ)") + }), + ) + + /** + * Widening the comparison window would compare the requested period against + * a differently-sized one, which is a wrong answer rather than a missing one. + */ + it.effect("never widens the previous window, even when a fallback strategy is set", () => + Effect.gen(function* () { + const { executor, requests } = makeExecutor((request) => + request.startTime === WINDOW.startTime + ? ok([point("2026-01-02T00:00:00.000Z", { all: 1 })]) + : ok([]), + ) + + yield* runTimeseriesQuerySet(executor, { + querySet: set({ comparison: { mode: "previous_period" } }), + ...WINDOW, + fallback: LAB_EMPTY_RANGE_STRATEGY, + }) + + // Primary succeeded on its first try, so the only other request is the + // single un-widened previous window. + assert.deepStrictEqual( + requests.map((r) => r.startTime), + [WINDOW.startTime, "2026-01-01 23:00:00"], + ) + }), + ) + }) +}) diff --git a/packages/query-engine/src/query-set/timeseries.ts b/packages/query-engine/src/query-set/timeseries.ts new file mode 100644 index 000000000..6d2e74c0f --- /dev/null +++ b/packages/query-engine/src/query-set/timeseries.ts @@ -0,0 +1,208 @@ +/** + * The whole timeseries path: run the set, merge, apply the comparison window. + * + * Everything a chart needs from a stored `QuerySet`, given only a host that can + * execute one `QuerySpec`. + */ + +import { Effect } from "effect" +import type { QueryComparisonMode, QuerySet } from "@maple/query-model" +import { parseWarehouseDateTime, formatWarehouseDateTime } from "../datetime" +import type { FormulaDraft, QueryRunResult } from "../formula-results" +import type { EmptyRangeFallbackStrategy } from "./bucketing" +import { NO_QUERY_DATA_MESSAGE, QuerySetInputError, QuerySetNoDataError } from "./errors" +import type { QuerySetExecutor } from "./port" +import { + appendPercentChangeSeries, + collectHiddenResultIds, + combineRows, + countSuccessfulQuerySeries, + hasAnySeriesData, + mergeQueryRunResults, + shiftRunResults, + toDisplayNameById, +} from "./series-merge" +import { runQuerySetWindow, type QuerySetQueryDiagnostics } from "./window" + +export interface TimeseriesQuerySetDiagnostics { + readonly primaryWindow: { readonly startTime: string; readonly endTime: string } + readonly comparison: { + readonly mode: QueryComparisonMode + readonly includePercentChange: boolean + readonly shiftedByMs: number + readonly previousStartTime: string | null + readonly previousEndTime: string | null + } + readonly queries: ReadonlyArray + readonly previousQueries: ReadonlyArray +} + +export interface TimeseriesQuerySetResult { + readonly rows: ReadonlyArray> + readonly diagnostics: TimeseriesQuerySetDiagnostics +} + +export interface RunTimeseriesQuerySetInput { + readonly querySet: QuerySet + readonly startTime: string + readonly endTime: string + /** Defaults to no widening. Only explore surfaces pass one. */ + readonly fallback?: EmptyRangeFallbackStrategy +} + +const OPERATION = "runTimeseriesQuerySet" + +/** + * Diagnostics are always computed, never gated behind a `debug` flag. + * + * They were already assembled unconditionally and then thrown away unless the + * caller asked; and since both hosts of this runner already hold the result + * in-process, returning them costs nothing on the wire. + */ +export const runTimeseriesQuerySet = Effect.fnUntraced(function* ( + executor: QuerySetExecutor, + input: RunTimeseriesQuerySetInput, +) { + const { querySet } = input + + const formulas: FormulaDraft[] = (querySet.formulas ?? []).map((formula) => ({ + id: formula.id, + name: formula.name, + expression: formula.expression, + legend: formula.legend, + })) + const hiddenResultIds = collectHiddenResultIds({ + queries: querySet.queries, + formulas: querySet.formulas, + }) + const isPlotted = (result: QueryRunResult): boolean => !hiddenResultIds.has(result.queryId) + const comparison = { + mode: querySet.comparison?.mode ?? ("none" as QueryComparisonMode), + includePercentChange: querySet.comparison?.includePercentChange ?? true, + } + + // `hidden` queries still run — a formula that references one needs its + // numbers — so only `enabled` filters here. + const enabledQueries = querySet.queries.filter((query) => query.enabled !== false) + if (enabledQueries.length === 0) { + return yield* new QuerySetInputError({ + operation: OPERATION, + message: "No enabled queries to run", + }) + } + + const currentWindow = yield* runQuerySetWindow(executor, { + queries: enabledQueries, + formulas, + startTime: input.startTime, + endTime: input.endTime, + ...(input.fallback === undefined ? {} : { fallback: input.fallback }), + }) + + if (countSuccessfulQuerySeries(currentWindow.queryResults) === 0) { + const details = currentWindow.queryResults.flatMap((result) => + typeof result.error === "string" && result.error.length > 0 ? [result.error] : [], + ) + return yield* new QuerySetNoDataError({ + message: details[0] ?? NO_QUERY_DATA_MESSAGE, + details, + }) + } + + const allResults = currentWindow.allResults + + if (!allResults.some((result) => result.status === "success" && hasAnySeriesData(result.data))) { + const firstError = allResults.find((result) => result.error)?.error + return yield* new QuerySetNoDataError({ + message: firstError ?? "No successful query results", + details: firstError === undefined || firstError === null ? [] : [firstError], + }) + } + + // Data came back, but nothing plottable did — say why rather than drawing an empty chart the + // reader would blame on the time range. On a ratio widget the plotted series is the formula, + // so its own failure (an unknown reference, no overlapping buckets) is the useful message. + if (!allResults.some((result) => isPlotted(result) && hasAnySeriesData(result.data))) { + const plottedError = allResults.find((result) => isPlotted(result) && result.error)?.error + return yield* new QuerySetInputError({ + operation: OPERATION, + message: plottedError ?? "Every query and formula with data is hidden — nothing to plot", + }) + } + + const displayNameById = toDisplayNameById([ + ...enabledQueries, + ...formulas.map((formula) => ({ + id: formula.id, + name: formula.name, + legend: formula.legend, + })), + ]) + + // ORDER MATTERS: `usedSeriesNames` is shared, and the current window must + // claim its names before the previous one, or ` (prev)` series would take the + // unsuffixed spellings. + const usedSeriesNames = new Set() + const mergedCurrent = mergeQueryRunResults(allResults.filter(isPlotted), displayNameById, { + usedSeriesNames, + }) + const mergedSets = [mergedCurrent] + + const startMs = parseWarehouseDateTime(input.startTime) + const endMs = parseWarehouseDateTime(input.endTime) + const shiftMs = Number.isFinite(startMs) && Number.isFinite(endMs) ? endMs - startMs : 0 + + let mergedPrevious: ReturnType | null = null + let previousStartTime: string | null = null + let previousEndTime: string | null = null + let previousDiagnostics: ReadonlyArray = [] + + if (comparison.mode === "previous_period" && shiftMs > 0) { + previousStartTime = formatWarehouseDateTime(startMs - shiftMs) + previousEndTime = formatWarehouseDateTime(endMs - shiftMs) + + // `allowFallback: false`: widening the comparison window would compare the + // requested period against a differently-sized one. + const previousWindow = yield* runQuerySetWindow(executor, { + queries: enabledQueries, + formulas, + startTime: previousStartTime, + endTime: previousEndTime, + allowFallback: false, + ...(input.fallback === undefined ? {} : { fallback: input.fallback }), + }) + previousDiagnostics = previousWindow.diagnostics + + const shiftedPreviousResults = shiftRunResults(previousWindow.allResults.filter(isPlotted), shiftMs) + mergedPrevious = mergeQueryRunResults(shiftedPreviousResults, displayNameById, { + seriesSuffix: " (prev)", + usedSeriesNames, + }) + mergedSets.push(mergedPrevious) + } + + const rows = combineRows(mergedSets) + if (comparison.mode === "previous_period" && comparison.includePercentChange && mergedPrevious) { + appendPercentChangeSeries( + rows, + mergedCurrent.seriesNameByStableKey, + mergedPrevious.seriesNameByStableKey, + ) + } + + return { + rows, + diagnostics: { + primaryWindow: { startTime: input.startTime, endTime: input.endTime }, + comparison: { + mode: comparison.mode, + includePercentChange: comparison.includePercentChange, + shiftedByMs: shiftMs > 0 ? shiftMs : 0, + previousStartTime, + previousEndTime, + }, + queries: currentWindow.diagnostics, + previousQueries: previousDiagnostics, + }, + } satisfies TimeseriesQuerySetResult +}) diff --git a/packages/query-engine/src/query-set/window.ts b/packages/query-engine/src/query-set/window.ts new file mode 100644 index 000000000..beb41af7d --- /dev/null +++ b/packages/query-engine/src/query-set/window.ts @@ -0,0 +1,265 @@ +/** + * Running every draft in a query set over ONE window, plus its formulas. + * + * The fan-out primitive the three surfaces share. It stops short of merging into + * rows on purpose: the MCP widget inspector reports per-query and would have to + * un-merge, while the chart paths merge immediately. Both call this; only the + * chart paths go on to `runTimeseriesQuerySet`. + */ + +import { Effect, Result } from "effect" +import type { QueryBuilderQueryDraftPayload } from "@maple/query-model" +import type { QuerySpec } from "@maple/domain/query-engine" +import { buildTimeseriesQuerySpec } from "../query-builder/model" +import { buildFormulaResults, type FormulaDraft, type QueryRunResult } from "../formula-results" +import { + type EmptyRangeFallbackStrategy, + type ExecutionWindow, + buildExecutionWindows, + NO_EMPTY_RANGE_FALLBACK, + resolveExecutionSpecForWindow, + resolveTimeseriesBucketSpec, +} from "./bucketing" +import type { QuerySetExecutor } from "./port" +import { countSuccessfulQuerySeries, hasAnySeriesData } from "./series-merge" + +/** One window attempt for one query, kept whether it succeeded or not. */ +export interface QuerySetAttempt { + readonly startTime: string + readonly endTime: string + readonly kind: ExecutionWindow["kind"] + readonly points: number + readonly hasSeries: boolean + readonly error?: string +} + +/** What one query actually did, for a debug panel or an agent-facing report. */ +export interface QuerySetQueryDiagnostics { + readonly queryId: string + readonly queryName: string + readonly source: string + readonly spec: QuerySpec | null + readonly attempts: ReadonlyArray + readonly fallbackUsed: boolean +} + +export interface QuerySetWindowResult { + readonly queryResults: ReadonlyArray + readonly formulaResults: ReadonlyArray + /** Queries and formulas together, in that order — what a merge consumes. */ + readonly allResults: ReadonlyArray + readonly diagnostics: ReadonlyArray +} + +/** + * Run one query over one window, widening through the fallback ladder if the + * strategy allows and the window came back empty. + * + * A PRIMARY-window failure propagates: the caller asked about that window and + * got no answer. A FALLBACK-window failure is recorded and skipped — the caller + * never asked about that window, so failing on it would turn a working chart + * into an error because a speculative widening was too expensive. + */ +const executeWithFallback = Effect.fnUntraced(function* ( + executor: QuerySetExecutor, + startTime: string, + endTime: string, + spec: QuerySpec, + strategy: EmptyRangeFallbackStrategy, + allowFallback: boolean, +) { + const windows = buildExecutionWindows(startTime, endTime, strategy, allowFallback) + const attempts: QuerySetAttempt[] = [] + let lastPoints: ReadonlyArray<{ bucket: string; series: Record }> = [] + + for (const [index, window] of windows.entries()) { + const windowSpec = resolveExecutionSpecForWindow(spec, window) + + const outcome = yield* Effect.result( + executor.execute({ startTime: window.startTime, endTime: window.endTime, query: windowSpec }), + ) + + if (Result.isFailure(outcome)) { + const error = outcome.failure + attempts.push({ + startTime: window.startTime, + endTime: window.endTime, + kind: window.kind, + points: 0, + hasSeries: false, + error: executor.describeError(error), + }) + + if (window.kind === "primary") { + return yield* Effect.fail(error) + } + continue + } + + const result = outcome.success + // A non-timeseries result for a timeseries spec is the host answering a + // different question; treat it as no data for this window rather than + // crashing the whole set. + const points = + result.kind === "timeseries" + ? result.data.map((point) => ({ bucket: point.bucket, series: { ...point.series } })) + : [] + const hasSeries = hasAnySeriesData(points) + + attempts.push({ + startTime: window.startTime, + endTime: window.endTime, + kind: window.kind, + points: points.length, + hasSeries, + }) + lastPoints = points + + if (hasSeries) { + return { points, attempts, fallbackUsed: index > 0 } + } + } + + return { points: lastPoints, attempts, fallbackUsed: false } +}) + +export interface RunQuerySetWindowInput { + readonly queries: ReadonlyArray + readonly formulas: ReadonlyArray + readonly startTime: string + readonly endTime: string + /** Defaults to no widening — see `EmptyRangeFallbackStrategy`. */ + readonly fallback?: EmptyRangeFallbackStrategy + /** Whether this window may widen at all. False for a previous-period window. */ + readonly allowFallback?: boolean +} + +/** + * Lower and execute every query, then evaluate the formulas over the results. + * + * Per-query failures are folded into `status: "error"` rather than failing the + * whole set, so one broken query does not blank a chart that has three working + * ones. Only a primary-window warehouse failure escapes as `E`. + * + * CONCURRENCY is `queries.length`, deliberately and not as a default to tune: + * the web adapter's executor is backed by a batcher that coalesces everything + * enqueued in the same tick into a single `POST /execute-batch`. Bounding + * concurrency here would silently turn one round trip into several. + */ +export const runQuerySetWindow = Effect.fnUntraced(function* ( + executor: QuerySetExecutor, + input: RunQuerySetWindowInput, +) { + const strategy = input.fallback ?? NO_EMPTY_RANGE_FALLBACK + const allowFallback = input.allowFallback ?? true + const diagnostics: QuerySetQueryDiagnostics[] = [] + + const queryResults = yield* Effect.forEach( + input.queries, + (query) => + Effect.gen(function* () { + const built = buildTimeseriesQuerySpec(query) + + if (!built.query) { + diagnostics.push({ + queryId: query.id, + queryName: query.name, + source: query.dataSource, + spec: null, + attempts: [], + fallbackUsed: false, + }) + + return { + queryId: query.id, + queryName: query.name, + source: query.dataSource, + status: "error", + error: built.error ?? "Failed to build query", + warnings: built.warnings, + data: [], + } satisfies QueryRunResult + } + + const querySpec = resolveTimeseriesBucketSpec(built.query, input.startTime, input.endTime) + + const outcome = yield* Effect.result( + executeWithFallback( + executor, + input.startTime, + input.endTime, + querySpec, + strategy, + allowFallback, + ), + ) + + if (Result.isFailure(outcome)) { + diagnostics.push({ + queryId: query.id, + queryName: query.name, + source: query.dataSource, + spec: querySpec, + attempts: [], + fallbackUsed: false, + }) + + return { + queryId: query.id, + queryName: query.name, + source: query.dataSource, + status: "error", + error: executor.describeError(outcome.failure), + warnings: built.warnings, + data: [], + } satisfies QueryRunResult + } + + const execution = outcome.success + diagnostics.push({ + queryId: query.id, + queryName: query.name, + source: query.dataSource, + spec: querySpec, + attempts: execution.attempts, + fallbackUsed: execution.fallbackUsed, + }) + + const warnings = [...built.warnings] + if (execution.fallbackUsed) { + const selectedAttempt = execution.attempts[execution.attempts.length - 1] + warnings.push( + `No data in requested range; used fallback window ${selectedAttempt.startTime} -> ${selectedAttempt.endTime}`, + ) + } + + return { + queryId: query.id, + queryName: query.name, + source: query.dataSource, + status: "success", + error: null, + warnings, + // error_rate arrives from the query engine as a 0–1 ratio — the + // canonical unit everywhere (the "percent" display unit multiplies + // by 100 when formatting). No rescaling here. + data: [...execution.points], + } satisfies QueryRunResult + }), + { concurrency: Math.max(input.queries.length, 1) }, + ) + + // Formulas divide by their operands, so evaluating them against a set where + // nothing returned data produces a page of NaN warnings rather than a result. + const formulaResults = + countSuccessfulQuerySeries(queryResults) > 0 + ? buildFormulaResults([...input.formulas], queryResults) + : [] + + return { + queryResults, + formulaResults, + allResults: [...queryResults, ...formulaResults], + diagnostics, + } satisfies QuerySetWindowResult +}) From f3605d4a0c2833db598e73a0680bbc16d2f784be Mon Sep 17 00:00:00 2001 From: Makisuo Date: Thu, 13 Aug 2026 23:23:57 +0200 Subject: [PATCH 11/13] refactor(query-engine): complete the query-set runner with breakdown and list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the two remaining shapes plus `runQuerySet`, which dispatches on the result shape a widget stores. Its input is structurally `WidgetQuerySet` — the return type of `dataSourceQuerySet` — so a stored widget feeds it with no adapter, and without @maple/query-engine importing @maple/widgets, which sits below @maple/domain and would invert the dependency. That is commented at the seam so nobody tidies it into an import. Two behavioural asymmetries are preserved rather than smoothed over, because each is right for its shape and unifying them would change a surface: - breakdown folds EVERY per-query failure into a status, so one broken query cannot blank a chart that has working ones; timeseries lets a primary-window failure escape. - list fails outright on an unexpected result kind, because it has one query and no sibling whose data would be lost. The list path gets test coverage for the first time. The three web server functions become adapters over one shared `makeWarehouseExecutor`, which keeps its per-shape span name — collapsing those would make existing traces harder to read for no gain. apps/web query-builder server functions: 1118 lines -> 275. --- .../api/warehouse/query-builder-breakdown.ts | 125 ++------- .../src/api/warehouse/query-builder-list.ts | 54 ++-- .../api/warehouse/query-builder-timeseries.ts | 39 +-- .../src/api/warehouse/query-set-executor.ts | 38 +++ .../query-engine/src/query-set/breakdown.ts | 136 +++++++++ .../src/query-set/dispatch.test.ts | 260 ++++++++++++++++++ .../query-engine/src/query-set/dispatch.ts | 72 +++++ packages/query-engine/src/query-set/index.ts | 3 + packages/query-engine/src/query-set/list.ts | 71 +++++ 9 files changed, 623 insertions(+), 175 deletions(-) create mode 100644 apps/web/src/api/warehouse/query-set-executor.ts create mode 100644 packages/query-engine/src/query-set/breakdown.ts create mode 100644 packages/query-engine/src/query-set/dispatch.test.ts create mode 100644 packages/query-engine/src/query-set/dispatch.ts create mode 100644 packages/query-engine/src/query-set/list.ts diff --git a/apps/web/src/api/warehouse/query-builder-breakdown.ts b/apps/web/src/api/warehouse/query-builder-breakdown.ts index c780f7985..b143385b0 100644 --- a/apps/web/src/api/warehouse/query-builder-breakdown.ts +++ b/apps/web/src/api/warehouse/query-builder-breakdown.ts @@ -1,10 +1,10 @@ -import { Effect, Result, Schema } from "effect" +import { Effect, Schema } from "effect" import { QueryBuilderQueryDraftSchema } from "@maple/domain/http" -import { QueryEngineExecuteRequest } from "@maple/query-engine" -import { buildBreakdownQuerySpec } from "@maple/query-engine/query-builder" -import { type BreakdownQueryResult, mergeBreakdownResults } from "@maple/query-engine/query-set" -import { decodeInput, executeQueryEngine, invalidWarehouseInput } from "@/api/warehouse/effect-utils" -import { displayError } from "@/lib/error-messages" +import { runBreakdownQuerySet } from "@maple/query-engine/query-set" +import { decodeInput, invalidWarehouseInput } from "@/api/warehouse/effect-utils" +import { makeWarehouseExecutor } from "@/api/warehouse/query-set-executor" + +const executor = makeWarehouseExecutor("queryEngine.breakdownQuery") const dateTimeString = Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)) @@ -23,77 +23,6 @@ const QueryBuilderBreakdownInputSchema = Schema.Struct({ export type QueryBuilderBreakdownInput = Schema.Schema.Type -const executeBreakdownQuery = Effect.fn("QueryEngine.executeBreakdownQuery")(function* ( - startTime: string, - endTime: string, - query: QueryBuilderBreakdownInput["queries"][number], - defaultLimit: number | undefined, -) { - const built = buildBreakdownQuerySpec(query, { defaultLimit }) - - if (!built.query) { - return { - queryId: query.id, - queryName: query.name, - status: "error", - error: built.error ?? "Failed to build breakdown query", - data: [], - } satisfies BreakdownQueryResult - } - - const request = yield* decodeInput( - QueryEngineExecuteRequest, - { - startTime, - endTime, - query: built.query, - }, - "executeBreakdownQuery.request", - ) - - // Per-query failures are folded into the result status rather than failing the - // whole batch, so one bad query doesn't blank the chart. Capture the outcome. - const outcome = yield* Effect.result(executeQueryEngine("queryEngine.breakdownQuery", request)) - - if (Result.isFailure(outcome)) { - const error = outcome.failure - return { - queryId: query.id, - queryName: query.name, - status: "error", - error: displayError(error).message, - data: [], - } satisfies BreakdownQueryResult - } - - const response = outcome.success - if (response.result.kind !== "breakdown") { - return { - queryId: query.id, - queryName: query.name, - status: "error", - error: "Unexpected non-breakdown result", - data: [], - } satisfies BreakdownQueryResult - } - - const mapped = response.result.data.map((item) => ({ - name: item.name, - value: item.value, - })) - - return { - queryId: query.id, - queryName: query.name, - status: "success", - error: null, - // error_rate arrives from the query engine as a 0–1 ratio — the canonical - // unit everywhere (the "percent" display unit multiplies by 100 when - // formatting). No rescaling here. - data: mapped, - } satisfies BreakdownQueryResult -}) - export function getQueryBuilderBreakdown({ data }: { data: QueryBuilderBreakdownInput }) { return getQueryBuilderBreakdownEffect({ data }) } @@ -105,34 +34,24 @@ const getQueryBuilderBreakdownEffect = Effect.fn("QueryEngine.getQueryBuilderBre }) { const input = yield* decodeInput(QueryBuilderBreakdownInputSchema, data, "getQueryBuilderBreakdown") - // Hidden breakdown queries feed no formulas, so do not execute them. - const enabledQueries = input.queries.filter((query) => query.enabled !== false && !query.hidden) - if (enabledQueries.length === 0) { - return yield* invalidWarehouseInput("getQueryBuilderBreakdown", "No enabled queries to run") - } - - const results = yield* Effect.forEach( - enabledQueries, - (query) => executeBreakdownQuery(input.startTime, input.endTime, query, input.defaultLimit), - { concurrency: enabledQueries.length }, + const outcome = yield* runBreakdownQuerySet(executor, { + querySet: { queries: input.queries }, + startTime: input.startTime, + endTime: input.endTime, + ...(input.defaultLimit === undefined ? {} : { defaultLimit: input.defaultLimit }), + }).pipe( + Effect.catchTags({ + "@maple/query-engine/query-set/QuerySetInputError": (error) => + invalidWarehouseInput("getQueryBuilderBreakdown", error.message), + "@maple/query-engine/query-set/QuerySetNoDataError": (error) => + invalidWarehouseInput("getQueryBuilderBreakdown", error.message), + }), ) - const firstError = results.find((r) => r.status === "error" && r.error)?.error - const anySuccess = results.some((r) => r.status === "success" && r.data.length > 0) - - if (!anySuccess) { - return yield* invalidWarehouseInput( - "getQueryBuilderBreakdown", - firstError ?? "No breakdown data found in selected time range", - ) - } - - return { - data: mergeBreakdownResults(results, enabledQueries), - } + return { data: [...outcome.rows] } }) -// The merge itself moved to `@maple/query-engine/query-set` and is tested there; -// what stays worth asserting here is that this module adds no rescaling of its -// own on the way out. +// The merge and the per-query execution moved to `@maple/query-engine/query-set` +// and are tested there; what stays worth asserting here is that this module adds +// no rescaling of its own on the way out. export const __testables = {} diff --git a/apps/web/src/api/warehouse/query-builder-list.ts b/apps/web/src/api/warehouse/query-builder-list.ts index d43fb4e57..be3af1c91 100644 --- a/apps/web/src/api/warehouse/query-builder-list.ts +++ b/apps/web/src/api/warehouse/query-builder-list.ts @@ -1,8 +1,8 @@ import { Effect, Schema } from "effect" import { QueryBuilderQueryDraftSchema } from "@maple/domain/http" -import { QueryEngineExecuteRequest } from "@maple/query-engine" -import { buildListQuerySpec } from "@maple/query-engine/query-builder" -import { decodeInput, executeQueryEngine, invalidWarehouseInput } from "@/api/warehouse/effect-utils" +import { runListQuerySet } from "@maple/query-engine/query-set" +import { decodeInput, invalidWarehouseInput } from "@/api/warehouse/effect-utils" +import { makeWarehouseExecutor } from "@/api/warehouse/query-set-executor" const dateTimeString = Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)) @@ -16,6 +16,8 @@ const QueryBuilderListInputSchema = Schema.Struct({ export type QueryBuilderListInput = Schema.Schema.Type +const executor = makeWarehouseExecutor("queryEngine.queryBuilderList") + export function getQueryBuilderList({ data }: { data: QueryBuilderListInput }) { return getQueryBuilderListEffect({ data }) } @@ -27,41 +29,17 @@ const getQueryBuilderListEffect = Effect.fn("QueryEngine.getQueryBuilderList")(f }) { const input = yield* decodeInput(QueryBuilderListInputSchema, data, "getQueryBuilderList") - const enabledQueries = input.queries.filter((q) => q.enabled !== false) - if (enabledQueries.length === 0) { - return yield* invalidWarehouseInput("getQueryBuilderList", "No enabled queries to run") - } - - const query = enabledQueries[0] - const built = buildListQuerySpec(query, input.limit, input.columns as string[] | undefined) - - if (!built.query) { - return yield* invalidWarehouseInput( - "getQueryBuilderList", - built.error ?? "Failed to build list query", - ) - } - - const request = yield* decodeInput( - QueryEngineExecuteRequest, - { - startTime: input.startTime, - endTime: input.endTime, - query: built.query, - }, - "getQueryBuilderList.request", + const outcome = yield* runListQuerySet(executor, { + querySet: { queries: input.queries }, + startTime: input.startTime, + endTime: input.endTime, + ...(input.limit === undefined ? {} : { limit: input.limit }), + ...(input.columns === undefined ? {} : { columns: input.columns }), + }).pipe( + Effect.catchTag("@maple/query-engine/query-set/QuerySetInputError", (error) => + invalidWarehouseInput("getQueryBuilderList", error.message), + ), ) - const response = yield* executeQueryEngine("queryEngine.queryBuilderList", request) - - if (response.result.kind !== "list") { - return yield* invalidWarehouseInput( - "getQueryBuilderList", - `Unexpected result kind: ${response.result.kind}`, - ) - } - - return { - data: response.result.data as Array>, - } + return { data: [...outcome.rows] } }) diff --git a/apps/web/src/api/warehouse/query-builder-timeseries.ts b/apps/web/src/api/warehouse/query-builder-timeseries.ts index 8b03c92cb..0db725b3a 100644 --- a/apps/web/src/api/warehouse/query-builder-timeseries.ts +++ b/apps/web/src/api/warehouse/query-builder-timeseries.ts @@ -1,23 +1,15 @@ import { Effect, Schema } from "effect" -import { QueryEngineExecuteRequest } from "@maple/query-engine" import { QueryBuilderQueryDraftSchema } from "@maple/domain/http" import { QueryBuilderFormulaSchema, QueryComparisonSchema } from "@maple/query-model" import { LAB_EMPTY_RANGE_STRATEGY, type EmptyRangeFallbackStrategy, - type QuerySetExecutor, type TimeseriesQuerySetDiagnostics, resolveFallbackStrategy, runTimeseriesQuerySet, } from "@maple/query-engine/query-set" -import { - decodeInput, - executeQueryEngine, - invalidWarehouseInput, - type BackendError, - type WarehouseApiError, -} from "@/api/warehouse/effect-utils" -import { displayError } from "@/lib/error-messages" +import { decodeInput, invalidWarehouseInput } from "@/api/warehouse/effect-utils" +import { makeWarehouseExecutor } from "@/api/warehouse/query-set-executor" /** * The browser-side adapter for `runTimeseriesQuerySet`. @@ -27,14 +19,12 @@ import { displayError } from "@/lib/error-messages" * left here is the three things that are genuinely this app's: * * 1. decoding the wire input, - * 2. building a `QuerySetExecutor` from the HTTP batcher, + * 2. naming the executor's span, * 3. mapping the runner's tagged failures back onto `WarehouseInvalidInputError` * with byte-identical messages, because `mapBuilderChartFailure` in the * alert-preview path still string-matches them. */ -type ExecuteError = WarehouseApiError | BackendError - const dateTimeString = Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)) const StrategySchema = Schema.Struct({ @@ -92,26 +82,7 @@ function resolveStrategy(input: QueryBuilderTimeseriesInput): EmptyRangeFallback ) } -/** - * The port, backed by the HTTP batcher. - * - * `executeQueryEngine` enqueues onto a per-tick batcher that coalesces every - * request made in the same tick into one `POST /execute-batch`. That is why the - * runner fans out at full concurrency rather than bounding itself. - */ -const warehouseExecutor: QuerySetExecutor = { - execute: (request) => - Effect.gen(function* () { - const decoded = yield* decodeInput( - QueryEngineExecuteRequest, - { startTime: request.startTime, endTime: request.endTime, query: request.query }, - "executeTimeseriesQuery.request", - ) - const response = yield* executeQueryEngine("queryEngine.timeseriesQuery", decoded) - return response.result - }), - describeError: (error) => displayError(error).message, -} +const executor = makeWarehouseExecutor("queryEngine.timeseriesQuery") export function getQueryBuilderTimeseries({ data }: { data: QueryBuilderTimeseriesInput }) { return getQueryBuilderTimeseriesEffect({ data }) @@ -125,7 +96,7 @@ const getQueryBuilderTimeseriesEffect = Effect.fn("QueryEngine.getQueryBuilderTi const input = yield* decodeInput(QueryBuilderTimeseriesInputSchema, data, "getQueryBuilderTimeseries") const strategy = resolveStrategy(input) - const outcome = yield* runTimeseriesQuerySet(warehouseExecutor, { + const outcome = yield* runTimeseriesQuerySet(executor, { querySet: { queries: input.queries, ...(input.formulas === undefined ? {} : { formulas: input.formulas }), diff --git a/apps/web/src/api/warehouse/query-set-executor.ts b/apps/web/src/api/warehouse/query-set-executor.ts new file mode 100644 index 000000000..d5df8e436 --- /dev/null +++ b/apps/web/src/api/warehouse/query-set-executor.ts @@ -0,0 +1,38 @@ +import { Effect } from "effect" +import { QueryEngineExecuteRequest } from "@maple/query-engine" +import type { QuerySetExecutor } from "@maple/query-engine/query-set" +import { + decodeInput, + executeQueryEngine, + type BackendError, + type WarehouseApiError, +} from "@/api/warehouse/effect-utils" +import { displayError } from "@/lib/error-messages" + +export type QuerySetExecuteError = WarehouseApiError | BackendError + +/** + * The browser's `QuerySetExecutor`: one `QuerySpec` over one window, via HTTP. + * + * `executeQueryEngine` enqueues onto a per-tick batcher that coalesces every + * request made in the same tick into a single `POST /execute-batch`. That is why + * the runners fan out at full concurrency instead of bounding themselves — see + * the note on `runQuerySetWindow`. + * + * Takes the operation label rather than hard-coding one: it lands on the span as + * `query.operation`, and collapsing the three shapes into a single name would + * make the existing traces harder to read for no gain. + */ +export const makeWarehouseExecutor = (operation: string): QuerySetExecutor => ({ + execute: (request) => + Effect.gen(function* () { + const decoded = yield* decodeInput( + QueryEngineExecuteRequest, + { startTime: request.startTime, endTime: request.endTime, query: request.query }, + `${operation}.request`, + ) + const response = yield* executeQueryEngine(operation, decoded) + return response.result + }), + describeError: (error) => displayError(error).message, +}) diff --git a/packages/query-engine/src/query-set/breakdown.ts b/packages/query-engine/src/query-set/breakdown.ts new file mode 100644 index 000000000..2634cfa14 --- /dev/null +++ b/packages/query-engine/src/query-set/breakdown.ts @@ -0,0 +1,136 @@ +/** + * The breakdown path: run each enabled query, merge into rows. + * + * Shaped differently from the timeseries path on purpose, and the difference is + * behavioural rather than stylistic — see the error note on + * `runBreakdownQuerySet`. + */ + +import { Effect, Result } from "effect" +import type { QuerySet } from "@maple/query-model" +import { buildBreakdownQuerySpec } from "../query-builder/model" +import { type BreakdownQueryResult, mergeBreakdownResults } from "./breakdown-merge" +import { QuerySetInputError, QuerySetNoDataError } from "./errors" +import type { QuerySetExecutor } from "./port" + +export interface BreakdownQuerySetResult { + readonly rows: ReadonlyArray> + readonly diagnostics: ReadonlyArray +} + +export interface RunBreakdownQuerySetInput { + readonly querySet: QuerySet + readonly startTime: string + readonly endTime: string + /** + * Rows to fetch per query when the author set no explicit limit add-on. + * A panel that collapses its long tail into an "Other" bucket asks for more + * rows than it draws, so that bucket is a real sum rather than absent; one + * that plots every row it receives omits this and keeps the warehouse default. + */ + readonly defaultLimit?: number +} + +const OPERATION = "runBreakdownQuerySet" + +/** + * Run a query set as a breakdown. + * + * NOTE the error asymmetry with `runTimeseriesQuerySet`, which is deliberate and + * predates this package: here EVERY per-query failure folds into + * `status: "error"`, including the first window's, so one bad query cannot blank + * a chart that has working ones. The timeseries path instead lets a + * primary-window failure escape. Making the two uniform is a behaviour change + * for one surface or the other and belongs in its own change, not in a move. + */ +export const runBreakdownQuerySet = Effect.fnUntraced(function* ( + executor: QuerySetExecutor, + input: RunBreakdownQuerySetInput, +) { + // Hidden breakdown queries feed no formulas, so — unlike timeseries — there is + // nothing to run them for. + const enabledQueries = input.querySet.queries.filter((query) => query.enabled !== false && !query.hidden) + if (enabledQueries.length === 0) { + return yield* new QuerySetInputError({ + operation: OPERATION, + message: "No enabled queries to run", + }) + } + + const results = yield* Effect.forEach( + enabledQueries, + (query) => + Effect.gen(function* () { + const built = buildBreakdownQuerySpec( + query, + input.defaultLimit === undefined ? undefined : { defaultLimit: input.defaultLimit }, + ) + + if (!built.query) { + return { + queryId: query.id, + queryName: query.name, + status: "error", + error: built.error ?? "Failed to build breakdown query", + data: [], + } satisfies BreakdownQueryResult + } + + const outcome = yield* Effect.result( + executor.execute({ + startTime: input.startTime, + endTime: input.endTime, + query: built.query, + }), + ) + + if (Result.isFailure(outcome)) { + return { + queryId: query.id, + queryName: query.name, + status: "error", + error: executor.describeError(outcome.failure), + data: [], + } satisfies BreakdownQueryResult + } + + const result = outcome.success + if (result.kind !== "breakdown") { + return { + queryId: query.id, + queryName: query.name, + status: "error", + error: `Unexpected result kind: ${result.kind}`, + data: [], + } satisfies BreakdownQueryResult + } + + return { + queryId: query.id, + queryName: query.name, + status: "success", + error: null, + // error_rate arrives from the query engine as a 0–1 ratio — the + // canonical unit everywhere (the "percent" display unit multiplies + // by 100 when formatting). No rescaling here. + data: result.data.map((item) => ({ name: item.name, value: item.value })), + } satisfies BreakdownQueryResult + }), + { concurrency: Math.max(enabledQueries.length, 1) }, + ) + + if (!results.some((r) => r.status === "success" && r.data.length > 0)) { + const details = results.flatMap((r) => + typeof r.error === "string" && r.error.length > 0 ? [r.error] : [], + ) + return yield* new QuerySetNoDataError({ + message: details[0] ?? "No breakdown data found in selected time range", + details, + }) + } + + return { + rows: mergeBreakdownResults(results, enabledQueries), + diagnostics: results, + } satisfies BreakdownQuerySetResult +}) diff --git a/packages/query-engine/src/query-set/dispatch.test.ts b/packages/query-engine/src/query-set/dispatch.test.ts new file mode 100644 index 000000000..ad495bee5 --- /dev/null +++ b/packages/query-engine/src/query-set/dispatch.test.ts @@ -0,0 +1,260 @@ +import { assert, describe, it } from "@effect/vitest" +import { Effect } from "effect" +import type { QueryEngineResult } from "@maple/domain/query-engine" +import type { QueryBuilderQueryDraftPayload, QuerySet } from "@maple/query-model" +import { runBreakdownQuerySet } from "./breakdown" +import { runQuerySet } from "./dispatch" +import { QuerySetInputError, QuerySetNoDataError } from "./errors" +import { runListQuerySet } from "./list" +import type { QuerySetExecutor, QuerySetExecuteRequest } from "./port" + +class FakeError { + constructor(readonly message: string) {} +} + +type FakeResponse = + | { readonly _tag: "ok"; readonly result: QueryEngineResult } + | { readonly _tag: "fail"; readonly error: FakeError } + +const breakdown = (data: ReadonlyArray<{ name: string; value: number }>): FakeResponse => ({ + _tag: "ok", + result: { kind: "breakdown", source: "traces", data }, +}) + +const list = (data: ReadonlyArray>): FakeResponse => ({ + _tag: "ok", + result: { kind: "list", source: "traces", data }, +}) + +const timeseries = ( + data: ReadonlyArray<{ bucket: string; series: Record }>, +): FakeResponse => ({ + _tag: "ok", + result: { kind: "timeseries", source: "traces", data }, +}) + +const fail = (message: string): FakeResponse => ({ _tag: "fail", error: new FakeError(message) }) + +const makeExecutor = (respond: (request: QuerySetExecuteRequest) => FakeResponse) => { + const requests: QuerySetExecuteRequest[] = [] + const executor: QuerySetExecutor = { + execute: (request) => + Effect.suspend((): Effect.Effect => { + requests.push(request) + const response = respond(request) + return response._tag === "fail" + ? Effect.fail(response.error) + : Effect.succeed(response.result) + }), + describeError: (error) => error.message, + } + return { executor, requests } +} + +// A breakdown needs a real group-by dimension, and the lowering only reads +// `groupBy` when its add-on is switched on — the same gate the builder UI uses. +const draft = (overrides: Partial = {}): QueryBuilderQueryDraftPayload => + ({ + id: "q-1", + name: "A", + dataSource: "traces", + aggregation: "count", + groupBy: ["service"], + addOns: { groupBy: true, having: false, orderBy: false, limit: false, legend: false }, + ...overrides, + }) as QueryBuilderQueryDraftPayload + +const set = (overrides: Partial = {}): QuerySet => ({ queries: [draft()], ...overrides }) + +const WINDOW = { startTime: "2026-01-02 00:00:00", endTime: "2026-01-02 01:00:00" } + +describe("runBreakdownQuerySet", () => { + it.effect("merges two queries into one row per name", () => + Effect.gen(function* () { + const { executor } = makeExecutor((request) => + request.query.kind === "breakdown" && request.query.source === "logs" + ? breakdown([{ name: "api", value: 5 }]) + : breakdown([{ name: "api", value: 9 }]), + ) + + const result = yield* runBreakdownQuerySet(executor, { + querySet: set({ + queries: [ + draft({ id: "t", name: "Traces" }), + draft({ id: "l", name: "Logs", dataSource: "logs" }), + ], + }), + ...WINDOW, + }) + + assert.deepStrictEqual(result.rows, [{ name: "api", Traces: 9, Logs: 5 }]) + }), + ) + + /** + * The asymmetry with the timeseries path: here a failing query never escapes, + * so one broken query cannot blank a chart that has a working one. + */ + it.effect("folds every per-query failure, including the first, into a status", () => + Effect.gen(function* () { + const { executor } = makeExecutor((request) => + request.query.kind === "breakdown" && request.query.source === "logs" + ? fail("logs are down") + : breakdown([{ name: "api", value: 9 }]), + ) + + const result = yield* runBreakdownQuerySet(executor, { + querySet: set({ + queries: [draft({ id: "l", dataSource: "logs" }), draft({ id: "t" })], + }), + ...WINDOW, + }) + + assert.strictEqual(result.diagnostics[0].status, "error") + assert.strictEqual(result.diagnostics[1].status, "success") + // One survivor, so it keeps the narrow single-query shape. + assert.deepStrictEqual(result.rows, [{ name: "api", value: 9 }]) + }), + ) + + it.effect("does not run hidden queries — nothing here consumes them", () => + Effect.gen(function* () { + const { executor, requests } = makeExecutor(() => breakdown([{ name: "api", value: 1 }])) + + yield* runBreakdownQuerySet(executor, { + querySet: set({ queries: [draft({ id: "a" }), draft({ id: "b", hidden: true })] }), + ...WINDOW, + }) + + assert.strictEqual(requests.length, 1) + }), + ) + + it.effect("fails with QuerySetNoDataError when nothing came back", () => + Effect.gen(function* () { + const { executor } = makeExecutor(() => breakdown([])) + const outcome = yield* Effect.result( + runBreakdownQuerySet(executor, { querySet: set(), ...WINDOW }), + ) + + assert.isTrue(outcome._tag === "Failure") + if (outcome._tag !== "Failure") return + assert.isTrue(outcome.failure instanceof QuerySetNoDataError) + }), + ) + + it.effect("passes defaultLimit through so an 'Other' bucket is a real sum", () => + Effect.gen(function* () { + const { executor, requests } = makeExecutor(() => breakdown([{ name: "api", value: 1 }])) + + yield* runBreakdownQuerySet(executor, { querySet: set(), ...WINDOW, defaultLimit: 50 }) + + const query = requests[0].query + assert.isTrue(query.kind === "breakdown") + if (query.kind !== "breakdown") return + assert.strictEqual(query.limit, 50) + }), + ) +}) + +describe("runListQuerySet", () => { + it.effect("returns the rows of the first enabled query", () => + Effect.gen(function* () { + const { executor, requests } = makeExecutor(() => list([{ TraceId: "abc" }])) + + const result = yield* runListQuerySet(executor, { + querySet: set({ + queries: [draft({ id: "off", enabled: false }), draft({ id: "on" })], + }), + ...WINDOW, + }) + + // The disabled query is skipped, not merged. + assert.strictEqual(requests.length, 1) + assert.deepStrictEqual(result.rows, [{ TraceId: "abc" }]) + }), + ) + + it.effect("forwards limit to the lowering", () => + Effect.gen(function* () { + const { executor, requests } = makeExecutor(() => list([])) + + yield* runListQuerySet(executor, { + querySet: set(), + ...WINDOW, + limit: 25, + columns: ["TraceId", "Duration"], + }) + + const query = requests[0].query + assert.isTrue(query.kind === "list") + if (query.kind !== "list") return + assert.strictEqual(query.limit, 25) + // `columns` is not asserted here: `buildListQuerySpec` attaches it through + // an `as QuerySpec` cast to a spec type that does not declare the field, + // so there is nothing type-safe to read it back through. That hole is the + // lowering's, not this runner's. + }), + ) + + it.effect("fails when nothing is enabled", () => + Effect.gen(function* () { + const { executor } = makeExecutor(() => list([])) + const outcome = yield* Effect.result( + runListQuerySet(executor, { + querySet: set({ queries: [draft({ enabled: false })] }), + ...WINDOW, + }), + ) + + assert.isTrue(outcome._tag === "Failure") + if (outcome._tag !== "Failure") return + assert.isTrue(outcome.failure instanceof QuerySetInputError) + }), + ) + + /** + * Unlike breakdown, a wrong result kind fails outright here: there is only one + * query, so no sibling's data is lost by failing. + */ + it.effect("fails on an unexpected result kind", () => + Effect.gen(function* () { + const { executor } = makeExecutor(() => breakdown([])) + const outcome = yield* Effect.result(runListQuerySet(executor, { querySet: set(), ...WINDOW })) + + assert.isTrue(outcome._tag === "Failure") + if (outcome._tag !== "Failure") return + assert.include(outcome.failure.message, "Unexpected result kind") + }), + ) +}) + +describe("runQuerySet", () => { + it.effect("dispatches each shape to its runner and tags the output", () => + Effect.gen(function* () { + const { executor } = makeExecutor((request) => { + if (request.query.kind === "breakdown") return breakdown([{ name: "api", value: 1 }]) + if (request.query.kind === "list") return list([{ TraceId: "abc" }]) + return timeseries([{ bucket: "2026-01-02T00:00:00.000Z", series: { all: 2 } }]) + }) + + const ts = yield* runQuerySet(executor, { + querySet: set(), + resultShape: "timeseries", + ...WINDOW, + }) + const bd = yield* runQuerySet(executor, { + querySet: set(), + resultShape: "breakdown", + ...WINDOW, + }) + const ls = yield* runQuerySet(executor, { querySet: set(), resultShape: "list", ...WINDOW }) + + assert.strictEqual(ts.shape, "timeseries") + assert.strictEqual(bd.shape, "breakdown") + assert.strictEqual(ls.shape, "list") + if (ls.shape !== "list") return + assert.deepStrictEqual(ls.rows, [{ TraceId: "abc" }]) + }), + ) +}) diff --git a/packages/query-engine/src/query-set/dispatch.ts b/packages/query-engine/src/query-set/dispatch.ts new file mode 100644 index 000000000..7da8c4dce --- /dev/null +++ b/packages/query-engine/src/query-set/dispatch.ts @@ -0,0 +1,72 @@ +/** + * One entry point keyed on the result shape a widget stores. + * + * Its input is structurally `WidgetQuerySet` (`@maple/widgets`'s + * `dataSourceQuerySet` return type): a `QuerySet` plus `resultShape` and the + * per-shape request-shaping fields. That is structural compatibility on purpose + * — `@maple/query-engine` must NOT import `@maple/widgets`, which sits below + * `@maple/domain` and would invert the dependency. Do not "tidy" this into an + * import. + */ + +import { Effect } from "effect" +import type { QueryResultShape, QuerySet } from "@maple/query-model" +import type { EmptyRangeFallbackStrategy } from "./bucketing" +import { type BreakdownQuerySetResult, runBreakdownQuerySet } from "./breakdown" +import { type ListQuerySetResult, runListQuerySet } from "./list" +import type { QuerySetExecutor } from "./port" +import { type TimeseriesQuerySetResult, runTimeseriesQuerySet } from "./timeseries" + +export type QuerySetRunOutput = + | ({ readonly shape: "timeseries" } & TimeseriesQuerySetResult) + | ({ readonly shape: "breakdown" } & BreakdownQuerySetResult) + | ({ readonly shape: "list" } & ListQuerySetResult) + +export interface RunQuerySetInput { + readonly querySet: QuerySet + readonly resultShape: QueryResultShape + readonly startTime: string + readonly endTime: string + /** Per-shape request shaping; each field is read by exactly one shape. */ + readonly defaultLimit?: number + readonly limit?: number + readonly columns?: ReadonlyArray + /** Timeseries only. Defaults to no widening. */ + readonly fallback?: EmptyRangeFallbackStrategy +} + +export const runQuerySet = Effect.fnUntraced(function* ( + executor: QuerySetExecutor, + input: RunQuerySetInput, +) { + switch (input.resultShape) { + case "timeseries": { + const result = yield* runTimeseriesQuerySet(executor, { + querySet: input.querySet, + startTime: input.startTime, + endTime: input.endTime, + ...(input.fallback === undefined ? {} : { fallback: input.fallback }), + }) + return { shape: "timeseries", ...result } satisfies QuerySetRunOutput + } + case "breakdown": { + const result = yield* runBreakdownQuerySet(executor, { + querySet: input.querySet, + startTime: input.startTime, + endTime: input.endTime, + ...(input.defaultLimit === undefined ? {} : { defaultLimit: input.defaultLimit }), + }) + return { shape: "breakdown", ...result } satisfies QuerySetRunOutput + } + case "list": { + const result = yield* runListQuerySet(executor, { + querySet: input.querySet, + startTime: input.startTime, + endTime: input.endTime, + ...(input.limit === undefined ? {} : { limit: input.limit }), + ...(input.columns === undefined ? {} : { columns: input.columns }), + }) + return { shape: "list", ...result } satisfies QuerySetRunOutput + } + } +}) diff --git a/packages/query-engine/src/query-set/index.ts b/packages/query-engine/src/query-set/index.ts index ea25434fd..f66cc73b1 100644 --- a/packages/query-engine/src/query-set/index.ts +++ b/packages/query-engine/src/query-set/index.ts @@ -20,9 +20,12 @@ // - Raw SQL. It is not a query set; it goes through `executeRawSql` with its // own reshaping. +export * from "./breakdown" export * from "./breakdown-merge" export * from "./bucketing" +export * from "./dispatch" export * from "./errors" +export * from "./list" export * from "./port" export * from "./series-merge" export * from "./timeseries" diff --git a/packages/query-engine/src/query-set/list.ts b/packages/query-engine/src/query-set/list.ts new file mode 100644 index 000000000..223a96a9c --- /dev/null +++ b/packages/query-engine/src/query-set/list.ts @@ -0,0 +1,71 @@ +/** + * The list path: raw rows from the first enabled query. + * + * Single-query by construction. A list has no value axis to merge on, so N + * queries would produce N unrelated tables rather than one — the builder UI + * reflects that by only ever offering one query for a list panel. + */ + +import { Effect } from "effect" +import type { QuerySet } from "@maple/query-model" +import { buildListQuerySpec } from "../query-builder/model" +import { QuerySetInputError } from "./errors" +import type { QuerySetExecutor } from "./port" + +export interface ListQuerySetResult { + readonly rows: ReadonlyArray> +} + +export interface RunListQuerySetInput { + readonly querySet: QuerySet + readonly startTime: string + readonly endTime: string + readonly limit?: number + readonly columns?: ReadonlyArray +} + +const OPERATION = "runListQuerySet" + +export const runListQuerySet = Effect.fnUntraced(function* ( + executor: QuerySetExecutor, + input: RunListQuerySetInput, +) { + const enabledQueries = input.querySet.queries.filter((query) => query.enabled !== false) + const query = enabledQueries[0] + if (query === undefined) { + return yield* new QuerySetInputError({ + operation: OPERATION, + message: "No enabled queries to run", + }) + } + + const built = buildListQuerySpec( + query, + input.limit, + input.columns === undefined ? undefined : [...input.columns], + ) + + if (!built.query) { + return yield* new QuerySetInputError({ + operation: OPERATION, + message: built.error ?? "Failed to build list query", + }) + } + + const result = yield* executor.execute({ + startTime: input.startTime, + endTime: input.endTime, + query: built.query, + }) + + // Unlike breakdown, a wrong result kind here fails outright: there is only one + // query, so there is no sibling whose data would be lost by failing. + if (result.kind !== "list") { + return yield* new QuerySetInputError({ + operation: OPERATION, + message: `Unexpected result kind: ${result.kind}`, + }) + } + + return { rows: result.data } satisfies ListQuerySetResult +}) From f857c0db3148152ea782467496df8842e30659a5 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Fri, 14 Aug 2026 01:11:49 +0200 Subject: [PATCH 12/13] refactor(widgets): make the widget data source a typed union MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the stored `{ endpoint, params }` bag with a discriminated union on `kind` — `query` | `raw_sql` | `route` | `static`. The endpoint string used to carry the source's identity and `params` its content, a contract enforced nowhere; each arm now declares the fields it actually has. The seam built for this paid off. `access.ts`'s accessors already read both shapes, so the ~30 call sites reading through them are untouched, and `construct.ts`'s ~40 writers — every dashboard template, the Perses importer, the MCP builders — needed no change either. The work was concentrated in the three places that still knew the raw shape. Notes on the shape of the change: - The v2 -> v3 transform is `upgrade-to-v3.ts`, deliberately NOT a `DashboardMigration`. The chain migrates lazily on every read for as long as a row goes unwritten, which is the wrong tool when the rows are rewritten once by a backfill. `migrateToLatest` now stamps the version it actually reached rather than claiming the current one. - Storage keeps `endpoint` open (`Schema.String`) on the route arm. Closing it would make one stale route name a decode failure, and the writable path refuses a rejected document — locking a whole dashboard out of editing. The typo check moved to `makeRouteDataSource`'s type parameter: open in storage, checked at authoring. - `/v2/dashboards` republishes the union rather than encoding back to the legacy wire. A breaking change, taken so there is one data-source shape rather than a wire format kept alive by a translation layer. `snakeCasedWire` keeps the query drafts' wire bytes identical while decoding them to real types, so they are now validated on the public API where before they rode inside an unvalidated bag. - `markdown_static` was a route pointing at a no-op server function purely so the registry lookup would succeed; it is `kind: "static"` now. - The web `WidgetDataSource` type dropped its `Omit<..., "endpoint"> & { endpoint }` hack, which collapses over a union. Registry totality and the typo check were two jobs bundled into one type; they are separated now. Legacy rows do not decode until the backfill runs. That window is deliberate and pinned by tests in DashboardPersistenceService.test.ts: a legacy row is refused loudly, and reads correctly once `upgradeStoredDocument` has been applied. Also adds a "Maple was updated" reload prompt, so a long-lived tab has a reason to pick up a new bundle instead of waiting to fail on a missing chunk. --- .../dashboard-templates/query-specs.test.ts | 25 +- .../src/mcp/lib/dashboard-mutations.test.ts | 2 +- apps/api/src/mcp/lib/raw-sql-widget.test.ts | 13 +- .../__tests__/dashboard-concurrency.test.ts | 2 +- apps/api/src/mcp/tools/inspect-chart-data.ts | 11 +- .../api/src/routes/v1/dashboards.http.test.ts | 2 +- .../api/src/routes/v2/dashboards.http.test.ts | 7 +- .../DashboardPersistenceService.test.ts | 61 ++--- .../dashboards/dashboard-changes.test.ts | 2 +- .../perses-dashboard-import.test.ts | 10 +- .../dashboards/perses-dashboard-import.ts | 4 +- .../list/dashboard-list.test.tsx | 16 +- .../list/dashboard-summary.test.ts | 16 +- .../portable-dashboard.test.ts | 2 +- .../dashboard-builder/portable-dashboard.ts | 19 +- .../sections/section-layout.test.ts | 2 +- .../src/components/dashboard-builder/types.ts | 29 ++- .../widgets/stat-widget.test.tsx | 2 +- .../widgets/types/markdown.tsx | 4 +- .../widgets/widget-definitions.test.ts | 27 ++- .../widgets/widget-definitions.ts | 4 +- .../components/layout/app-update-banner.tsx | 48 ++++ .../components/layout/dashboard-layout.tsx | 7 +- apps/web/src/hooks/use-app-version.ts | 98 ++++++++ .../src/hooks/use-dashboard-store.test.tsx | 5 +- apps/web/src/hooks/use-widget-data.ts | 17 +- .../lib/dashboards/section-view-state.test.ts | 11 +- .../lib/models/dashboards-list-model.test.ts | 8 +- .../widget-builder-utils.test.ts | 47 ++-- .../query-builder/widget-type-cycle.test.ts | 28 ++- apps/web/src/worker.ts | 9 + apps/web/vite-plugin-version-manifest.ts | 32 +++ apps/web/vite.config.ts | 6 + .../domain/src/dashboard-variables.test.ts | 2 +- .../http/v2/dashboard-widget-parity.test.ts | 85 ++++--- packages/domain/src/http/v2/dashboards.ts | 86 ++++++- .../domain/src/http/v2/v2-contract.test.ts | 16 +- packages/widgets/src/dashboard/access.ts | 25 +- .../widgets/src/dashboard/construct.test.ts | 26 ++- packages/widgets/src/dashboard/construct.ts | 96 +++++--- .../widgets/src/dashboard/document-helpers.ts | 8 +- packages/widgets/src/dashboard/index.ts | 37 ++- .../widgets/src/dashboard/legacy-endpoints.ts | 55 +++++ .../widgets/src/dashboard/migrations/index.ts | 19 +- .../dashboard/migrations/migrations.test.ts | 58 +++-- packages/widgets/src/dashboard/parse.ts | 6 +- .../src/dashboard/upgrade-to-v3.test.ts | 214 ++++++++++++++++++ .../widgets/src/dashboard/upgrade-to-v3.ts | 208 +++++++++++++++++ .../widgets/src/dashboard/v3/data-source.ts | 147 ++++++++++++ packages/widgets/src/dashboard/v3/document.ts | 18 ++ packages/widgets/src/dashboard/v3/widget.ts | 20 ++ packages/widgets/src/dashboard/version.ts | 4 +- 52 files changed, 1439 insertions(+), 267 deletions(-) create mode 100644 apps/web/src/components/layout/app-update-banner.tsx create mode 100644 apps/web/src/hooks/use-app-version.ts create mode 100644 apps/web/vite-plugin-version-manifest.ts create mode 100644 packages/widgets/src/dashboard/legacy-endpoints.ts create mode 100644 packages/widgets/src/dashboard/upgrade-to-v3.test.ts create mode 100644 packages/widgets/src/dashboard/upgrade-to-v3.ts create mode 100644 packages/widgets/src/dashboard/v3/data-source.ts create mode 100644 packages/widgets/src/dashboard/v3/document.ts create mode 100644 packages/widgets/src/dashboard/v3/widget.ts diff --git a/apps/api/src/dashboard-templates/query-specs.test.ts b/apps/api/src/dashboard-templates/query-specs.test.ts index fa02ec525..56b5f7df7 100644 --- a/apps/api/src/dashboard-templates/query-specs.test.ts +++ b/apps/api/src/dashboard-templates/query-specs.test.ts @@ -38,11 +38,13 @@ function allParams(template: TemplateDefinition): TemplateParameterValues { return values } +// v3 replaced the `custom_query_builder_*` endpoint names with `kind: "query"` +// plus a `resultShape`, so the spec builder is selected by shape. function specBuilderFor( - endpoint: string, + resultShape: string, ): ((query: Parameters[0]) => BuildSpecResult) | null { - if (endpoint === "custom_query_builder_timeseries") return buildTimeseriesQuerySpec - if (endpoint === "custom_query_builder_breakdown") return buildBreakdownQuerySpec + if (resultShape === "timeseries") return buildTimeseriesQuerySpec + if (resultShape === "breakdown") return buildBreakdownQuerySpec return null } @@ -50,11 +52,13 @@ describe("dashboard template query specs", () => { function checkTemplate(template: TemplateDefinition, params: TemplateParameterValues, variant: string) { const built = template.build(params) for (const widget of built.widgets) { - const buildSpec = specBuilderFor(widget.dataSource.endpoint) + const dataSource = widget.dataSource + if (dataSource.kind !== "query") continue + const buildSpec = specBuilderFor(dataSource.resultShape) if (!buildSpec) continue - const rawQueries = widget.dataSource.params?.queries - expect(Array.isArray(rawQueries), `${template.id}/${widget.id}: params.queries`).toBe(true) + const rawQueries = dataSource.queries + expect(Array.isArray(rawQueries), `${template.id}/${widget.id}: queries`).toBe(true) if (!Array.isArray(rawQueries)) continue expect(rawQueries.length, `${template.id}/${widget.id}`).toBeGreaterThan(0) @@ -80,7 +84,7 @@ describe("dashboard template query specs", () => { }) } - // If the query-builder endpoints are ever renamed, the loop above would + // If the query-builder result shapes are ever renamed, the loop above would // silently skip everything and the guard would pass vacuously. Recount here // (template building is pure) instead of sharing a mutable counter across // tests — that breaks under shuffle, sharding, and `it.only`. @@ -89,9 +93,10 @@ describe("dashboard template query specs", () => { for (const template of DASHBOARD_TEMPLATES) { const built = template.build(sampleParams(template)) for (const widget of built.widgets) { - if (!specBuilderFor(widget.dataSource.endpoint)) continue - const rawQueries = widget.dataSource.params?.queries - if (Array.isArray(rawQueries)) queryBuilderQueries += rawQueries.length + const dataSource = widget.dataSource + if (dataSource.kind !== "query") continue + if (!specBuilderFor(dataSource.resultShape)) continue + queryBuilderQueries += dataSource.queries.length } } expect(queryBuilderQueries).toBeGreaterThan(0) diff --git a/apps/api/src/mcp/lib/dashboard-mutations.test.ts b/apps/api/src/mcp/lib/dashboard-mutations.test.ts index e869c148b..7a34d62a5 100644 --- a/apps/api/src/mcp/lib/dashboard-mutations.test.ts +++ b/apps/api/src/mcp/lib/dashboard-mutations.test.ts @@ -68,7 +68,7 @@ const NOW = asIsoDateTimeString(new Date("2026-01-01T00:00:00.000Z").toISOString const widget = (id: string) => ({ id, visualization: "stat", - dataSource: { endpoint: "test" }, + dataSource: { kind: "route", endpoint: "test" }, display: {}, layout: { x: 0, y: 0, w: 3, h: 4 }, }) diff --git a/apps/api/src/mcp/lib/raw-sql-widget.test.ts b/apps/api/src/mcp/lib/raw-sql-widget.test.ts index 299b2fae2..c000cec3d 100644 --- a/apps/api/src/mcp/lib/raw-sql-widget.test.ts +++ b/apps/api/src/mcp/lib/raw-sql-widget.test.ts @@ -25,18 +25,19 @@ describe("visualizationToDisplayType", () => { }) describe("buildRawSqlDataSource", () => { - it("returns a raw_sql_chart dataSource with required params", () => { + it("returns a raw_sql data source with its fields hoisted", () => { const result = buildRawSqlDataSource({ visualization: "chart", sql: "SELECT 1 WHERE $__orgFilter", displayType: "line", }) - expect(result.endpoint).toBe("raw_sql_chart") - expect(result.params).toEqual({ + // v3: no endpoint string, and the payload sits on the arm itself rather + // than inside an opaque `params` bag. + expect(result).toEqual({ + kind: "raw_sql", sql: "SELECT 1 WHERE $__orgFilter", displayType: "line", }) - expect(result.transform).toBeUndefined() }) it("includes granularitySeconds when provided", () => { @@ -46,7 +47,7 @@ describe("buildRawSqlDataSource", () => { displayType: "line", granularitySeconds: 60, }) - expect(result.params?.granularitySeconds).toBe(60) + expect(result.granularitySeconds).toBe(60) }) it("omits granularitySeconds when null/undefined", () => { @@ -55,7 +56,7 @@ describe("buildRawSqlDataSource", () => { sql: "SELECT 1 WHERE $__orgFilter", displayType: "line", }) - expect(result.params).not.toHaveProperty("granularitySeconds") + expect(result).not.toHaveProperty("granularitySeconds") }) it("auto-injects reduceToValue transform for stat widgets", () => { diff --git a/apps/api/src/mcp/tools/__tests__/dashboard-concurrency.test.ts b/apps/api/src/mcp/tools/__tests__/dashboard-concurrency.test.ts index d089b4ff6..b35e8f762 100644 --- a/apps/api/src/mcp/tools/__tests__/dashboard-concurrency.test.ts +++ b/apps/api/src/mcp/tools/__tests__/dashboard-concurrency.test.ts @@ -62,7 +62,7 @@ const NOW = asIsoDateTimeString(new Date("2026-01-01T00:00:00.000Z").toISOString const widget = (id: string) => ({ id, visualization: "stat", - dataSource: { endpoint: "test" }, + dataSource: { kind: "route", endpoint: "test" }, display: {}, layout: { x: 0, y: 0, w: 3, h: 4 }, }) diff --git a/apps/api/src/mcp/tools/inspect-chart-data.ts b/apps/api/src/mcp/tools/inspect-chart-data.ts index 3e6fa2efa..6bb9880ee 100644 --- a/apps/api/src/mcp/tools/inspect-chart-data.ts +++ b/apps/api/src/mcp/tools/inspect-chart-data.ts @@ -70,11 +70,12 @@ function unsupportedEndpointResult( widget: { id: string visualization: string - dataSource: { - endpoint: string - params?: Record - transform?: Record - } + // `unknown`, because that is all this function needs: it hands the value to + // `dataSourceEndpoint` (which narrows internally) and to `JSON.stringify`. + // Spelling out a shape here only ever pinned it to one schema version — and + // this is the diagnostic path for a widget nothing else could read, so it is + // the last place that should care what shape the data source is in. + dataSource: unknown display: { title?: string; unit?: string } }, dashboardName: string, diff --git a/apps/api/src/routes/v1/dashboards.http.test.ts b/apps/api/src/routes/v1/dashboards.http.test.ts index 2dd8c3a1c..a7c3fdf49 100644 --- a/apps/api/src/routes/v1/dashboards.http.test.ts +++ b/apps/api/src/routes/v1/dashboards.http.test.ts @@ -87,7 +87,7 @@ const makeHarness = () => { const widget = (id: string, overrides: Record = {}) => ({ id, visualization: "chart", - dataSource: { endpoint: "traces_timeseries", params: {} }, + dataSource: { kind: "route", endpoint: "traces_timeseries" }, display: { title: id, chartPresentation: { legend: "visible" } }, layout: { x: 0, y: 0, w: 6, h: 4 }, ...overrides, diff --git a/apps/api/src/routes/v2/dashboards.http.test.ts b/apps/api/src/routes/v2/dashboards.http.test.ts index 7aa78ae32..b39822230 100644 --- a/apps/api/src/routes/v2/dashboards.http.test.ts +++ b/apps/api/src/routes/v2/dashboards.http.test.ts @@ -222,7 +222,7 @@ describe("v2 dashboards over HTTP", () => { const widget = (id: string, timeRange?: unknown) => ({ id, visualization: "stat", - data_source: { endpoint: "custom_query_builder_timeseries" }, + data_source: { kind: "query", result_shape: "timeseries", queries: [] }, display: { title: id }, layout: { x: 0, y: 0, w: 3, h: 3 }, ...(timeRange !== undefined ? { time_range: timeRange } : {}), @@ -332,7 +332,8 @@ describe("v2 dashboards over HTTP", () => { // One real widget per preview-metadata entry, carrying the data source the // browser needs to evaluate it. expect(preview.body.widgets.length).toBe(postgres.preview.length) - expect(preview.body.widgets[0].data_source.endpoint.length).toBeGreaterThan(0) + // v3: identity is `kind`, and only the `route` arm still carries an endpoint. + expect(preview.body.widgets[0].data_source.kind.length).toBeGreaterThan(0) expect("timeRange" in preview.body).toBe(false) // Nothing was persisted. @@ -374,7 +375,7 @@ describe("v2 dashboards over HTTP", () => { { id: "error-rate", visualization: "chart", - data_source: { endpoint: "traces_timeseries", params: {} }, + data_source: { kind: "route", endpoint: "traces_timeseries" }, // `fill_nulls` is `number | false`; `true` is not a member. display: { title: "error-rate", chart_presentation: { fill_nulls: true } }, layout: { x: 0, y: 0, w: 6, h: 4 }, diff --git a/apps/api/src/services/dashboards/DashboardPersistenceService.test.ts b/apps/api/src/services/dashboards/DashboardPersistenceService.test.ts index f1fc6cd6d..973390e7f 100644 --- a/apps/api/src/services/dashboards/DashboardPersistenceService.test.ts +++ b/apps/api/src/services/dashboards/DashboardPersistenceService.test.ts @@ -14,7 +14,7 @@ import { import { Database, DatabaseError } from "@/platform/DatabaseLive" import { DashboardPersistenceService } from "./DashboardPersistenceService" import { Env } from "@/platform/Env" -import { CURRENT_DASHBOARD_SCHEMA_VERSION } from "@maple/widgets/dashboard" +import { CURRENT_DASHBOARD_SCHEMA_VERSION, upgradeStoredDocument } from "@maple/widgets/dashboard" import { cleanupTestDbs, createTestDb, executeSql, queryFirstRow, type TestDb } from "@/platform/test-pglite" const trackedDbs: TestDb[] = [] @@ -231,7 +231,7 @@ describe("DashboardPersistenceService", () => { const widget = (id: string, membership: Record) => ({ id, visualization: "chart", - dataSource: { endpoint: "custom_query_builder_timeseries" }, + dataSource: { kind: "query", resultShape: "timeseries", queries: [] }, display: {}, layout: { x: 0, y: 0, w: 4, h: 4 }, ...membership, @@ -297,7 +297,7 @@ describe("DashboardPersistenceService", () => { { id: "w1", visualization: "chart", - dataSource: { endpoint: "custom_query_builder_timeseries" }, + dataSource: { kind: "query", resultShape: "timeseries", queries: [] }, display: {}, layout: { x: 0, y: 0, w: 6, h: 4 }, sectionId: "s1", @@ -359,7 +359,9 @@ describe("DashboardPersistenceService", () => { visualization: "chart", dataSource: { endpoint: "custom_query_builder_timeseries", - params: { queries: [{ id: "a", name: "A", dataSource: "traces" }] }, + params: { + queries: [{ id: "a", name: "A", dataSource: "traces", aggregation: "count" }], + }, }, display: {}, layout: { x: 0, y: 0, w: 6, h: 4 }, @@ -384,46 +386,51 @@ describe("DashboardPersistenceService", () => { }).pipe(Effect.provide(makeLayer(testDb))) }) - it.effect("reads a legacy payload that predates the version marker", () => { + // These two tests used to assert lazy upgrade: a legacy row read fine and was + // rewritten at its next natural write. Schema v3 deliberately ends that. The + // v2 -> v3 step is a one-shot backfill rather than a migration-chain entry, so + // this build cannot read a row the backfill has not touched — and that gap IS + // the migration window. The pair below pins both halves of it so the window is + // a documented, tested property rather than a surprise in production. + it.effect("refuses a legacy row the backfill has not converted yet", () => { const testDb = createTestDb(trackedDbs) return Effect.gen(function* () { yield* insertRawDashboard(testDb, "dash-legacy", legacyPayload("dash-legacy")) - const dashboard = yield* DashboardPersistenceService.get( - asOrgId("org_a"), - asDashboardId("dash-legacy"), + const outcome = yield* Effect.exit( + DashboardPersistenceService.get(asOrgId("org_a"), asDashboardId("dash-legacy")), ) - assert.strictEqual(dashboard.name, "Legacy") - // Params are carried through the migration byte-for-byte. - assert.deepStrictEqual(dashboard.widgets[0]?.dataSource.params, { - queries: [{ id: "a", name: "A", dataSource: "traces" }], - }) + // Loudly, not silently. A half-decoded document would be persisted by + // the next read-modify-write and the original lost. + assert.strictEqual(outcome._tag, "Failure") }).pipe(Effect.provide(makeLayer(testDb))) }) - it.effect("upgrades a legacy row lazily — on its next write, not on read", () => { + it.effect("reads that same row once the backfill transform has been applied", () => { const testDb = createTestDb(trackedDbs) return Effect.gen(function* () { - yield* insertRawDashboard(testDb, "dash-lazy", legacyPayload("dash-lazy")) - - yield* DashboardPersistenceService.get(asOrgId("org_a"), asDashboardId("dash-lazy")) - const afterRead = yield* readStoredPayload(testDb, "dash-lazy") - assert.strictEqual( - afterRead?.payload_json.schemaVersion, - undefined, - "a read must not rewrite storage", + // Exactly what `backfill-dashboard-datasource-v3.ts` writes back. + yield* insertRawDashboard( + testDb, + "dash-backfilled", + upgradeStoredDocument(legacyPayload("dash-backfilled")) as Record, ) - yield* DashboardPersistenceService.upsert( + const dashboard = yield* DashboardPersistenceService.get( asOrgId("org_a"), - asUserId("user_a"), - makeDashboard({ id: asDashboardId("dash-lazy"), name: "Rewritten" }), + asDashboardId("dash-backfilled"), ) - const afterWrite = yield* readStoredPayload(testDb, "dash-lazy") - assert.strictEqual(afterWrite?.payload_json.schemaVersion, CURRENT_DASHBOARD_SCHEMA_VERSION) + + assert.strictEqual(dashboard.name, "Legacy") + const dataSource = dashboard.widgets[0]?.dataSource + assert.strictEqual(dataSource?.kind, "query") + // The queries survive the reshaping; only the envelope changed. + assert.deepStrictEqual(dataSource.kind === "query" ? dataSource.queries : undefined, [ + { id: "a", name: "A", dataSource: "traces", aggregation: "count" }, + ]) }).pipe(Effect.provide(makeLayer(testDb))) }) diff --git a/apps/api/src/services/dashboards/dashboard-changes.test.ts b/apps/api/src/services/dashboards/dashboard-changes.test.ts index 787651fe8..5ff621be1 100644 --- a/apps/api/src/services/dashboards/dashboard-changes.test.ts +++ b/apps/api/src/services/dashboards/dashboard-changes.test.ts @@ -65,7 +65,7 @@ const errors = { id: "s2", title: "Errors", tabs: [{ id: "t2", title: "Rates" }] const sectionWidget = (id: string, membership: Record = {}) => ({ id, visualization: "chart", - dataSource: { endpoint: "custom_query_builder_timeseries" }, + dataSource: { kind: "query", resultShape: "timeseries", queries: [] }, display: {}, layout: { x: 0, y: 0, w: 4, h: 4 }, ...membership, diff --git a/apps/api/src/services/dashboards/perses-dashboard-import.test.ts b/apps/api/src/services/dashboards/perses-dashboard-import.test.ts index 236710831..08813b4cb 100644 --- a/apps/api/src/services/dashboards/perses-dashboard-import.test.ts +++ b/apps/api/src/services/dashboards/perses-dashboard-import.test.ts @@ -103,8 +103,11 @@ describe("convertPersesDashboardToPortable", () => { assert.deepStrictEqual(result.dashboard.tags, ["perses-import"]) assert.deepStrictEqual(result.dashboard.timeRange, { type: "relative", value: "6h" }) assert.strictEqual(widget.visualization, "chart") - assert.strictEqual(widget.dataSource.endpoint, "raw_sql_chart") - assert.strictEqual((widget.dataSource.params as Record).displayType, "line") + assert.strictEqual(widget.dataSource.kind, "raw_sql") + assert.strictEqual( + widget.dataSource.kind === "raw_sql" ? widget.dataSource.displayType : undefined, + "line", + ) assert.strictEqual(widget.display.title, "Requests") assert.strictEqual(widget.layout.x, 10) assert.strictEqual(widget.layout.w, 2) @@ -271,7 +274,8 @@ describe("convertPersesDashboardToPortable", () => { const result = yield* convertPersesDashboardToPortable(input) const widget = result.dashboard.widgets[0]! - const params = widget.dataSource.params as Record + if (widget.dataSource.kind !== "raw_sql") throw new Error("expected a raw_sql data source") + const params: Record = widget.dataSource assert.strictEqual(widget.visualization, "table") assert.strictEqual(params.displayType, "table") diff --git a/apps/api/src/services/dashboards/perses-dashboard-import.ts b/apps/api/src/services/dashboards/perses-dashboard-import.ts index 36f051fdb..dbb75c849 100644 --- a/apps/api/src/services/dashboards/perses-dashboard-import.ts +++ b/apps/api/src/services/dashboards/perses-dashboard-import.ts @@ -10,7 +10,7 @@ import { widgetTypeByVisualization, type WidgetVisualization, } from "@maple/domain/http" -import { makeRawSqlDataSource, makeRouteDataSource } from "@maple/widgets/dashboard" +import { makeRawSqlDataSource, makeRouteDataSource, makeStaticDataSource } from "@maple/widgets/dashboard" type UnknownRecord = Record type DashboardWidget = typeof DashboardWidgetSchema.Type @@ -293,7 +293,7 @@ function rawSqlDataSource(args: { } function markdownDataSource(): DashboardWidget["dataSource"] { - return makeRouteDataSource("markdown_static") + return makeStaticDataSource() } function markdownWidgetContent(args: { diff --git a/apps/web/src/components/dashboard-builder/list/dashboard-list.test.tsx b/apps/web/src/components/dashboard-builder/list/dashboard-list.test.tsx index 493e40b7a..bf5285aa9 100644 --- a/apps/web/src/components/dashboard-builder/list/dashboard-list.test.tsx +++ b/apps/web/src/components/dashboard-builder/list/dashboard-list.test.tsx @@ -1,3 +1,4 @@ +import { QUERY_ENDPOINT_SHAPES } from "@maple/widgets/dashboard" // @vitest-environment jsdom import { cleanup, fireEvent, render, screen } from "@testing-library/react" @@ -20,10 +21,23 @@ afterEach(cleanup) let seq = 0 +// The call sites still name an endpoint, because that is what these tests are +// about (which tiles a dashboard summary counts). v3 moved the identity onto +// `kind`, so the name is mapped to the matching arm here rather than at 20 call +// sites. +const dataSourceFor = (endpoint: DataSourceEndpoint): DashboardWidget["dataSource"] => { + if (endpoint === "markdown_static") return { kind: "static" } + if (endpoint === "raw_sql_chart") return { kind: "raw_sql", sql: "SELECT 1" } + const shape = QUERY_ENDPOINT_SHAPES[endpoint] + return shape === undefined + ? { kind: "route", endpoint } + : { kind: "query", resultShape: shape, queries: [] } +} + const widget = (visualization: VisualizationType, endpoint: DataSourceEndpoint): DashboardWidget => ({ id: `w${++seq}`, visualization, - dataSource: { endpoint }, + dataSource: dataSourceFor(endpoint), display: { title: visualization }, layout: { x: 0, y: 0, w: 4, h: 4 }, }) diff --git a/apps/web/src/components/dashboard-builder/list/dashboard-summary.test.ts b/apps/web/src/components/dashboard-builder/list/dashboard-summary.test.ts index 9c6c1d3c4..25cdcc800 100644 --- a/apps/web/src/components/dashboard-builder/list/dashboard-summary.test.ts +++ b/apps/web/src/components/dashboard-builder/list/dashboard-summary.test.ts @@ -1,3 +1,4 @@ +import { QUERY_ENDPOINT_SHAPES } from "@maple/widgets/dashboard" import { describe, expect, it } from "vitest" import type { Dashboard, @@ -18,10 +19,23 @@ import { let widgetSeq = 0 +// The call sites still name an endpoint, because that is what these tests are +// about (which tiles a dashboard summary counts). v3 moved the identity onto +// `kind`, so the name is mapped to the matching arm here rather than at 20 call +// sites. +const dataSourceFor = (endpoint: DataSourceEndpoint): DashboardWidget["dataSource"] => { + if (endpoint === "markdown_static") return { kind: "static" } + if (endpoint === "raw_sql_chart") return { kind: "raw_sql", sql: "SELECT 1" } + const shape = QUERY_ENDPOINT_SHAPES[endpoint] + return shape === undefined + ? { kind: "route", endpoint } + : { kind: "query", resultShape: shape, queries: [] } +} + const widget = (visualization: VisualizationType, endpoint: DataSourceEndpoint): DashboardWidget => ({ id: `w${++widgetSeq}`, visualization, - dataSource: { endpoint }, + dataSource: dataSourceFor(endpoint), display: { title: visualization }, layout: { x: 0, y: 0, w: 4, h: 4 }, }) diff --git a/apps/web/src/components/dashboard-builder/portable-dashboard.test.ts b/apps/web/src/components/dashboard-builder/portable-dashboard.test.ts index b36ffb835..e49fde284 100644 --- a/apps/web/src/components/dashboard-builder/portable-dashboard.test.ts +++ b/apps/web/src/components/dashboard-builder/portable-dashboard.test.ts @@ -98,7 +98,7 @@ describe("portable-dashboard", () => { { id: "w1", visualization: "chart", - dataSource: { endpoint: "custom_query_builder_timeseries" }, + dataSource: { kind: "query", resultShape: "timeseries", queries: [] }, display: {}, layout: { x: 0, y: 0, w: 6, h: 4 }, sectionId: "s1", diff --git a/apps/web/src/components/dashboard-builder/portable-dashboard.ts b/apps/web/src/components/dashboard-builder/portable-dashboard.ts index a28eeff25..0ae5277cc 100644 --- a/apps/web/src/components/dashboard-builder/portable-dashboard.ts +++ b/apps/web/src/components/dashboard-builder/portable-dashboard.ts @@ -81,17 +81,20 @@ export function toPortableDashboard(dashboard: Dashboard): PortableDashboard { } } -// Deliberately NOT routed through `dataSourceRouteParams`: this is defensive -// hygiene over hand-written or externally-produced portable JSON, where a baked -// absolute window can appear under any endpoint, not just a curated route. It -// reads the stored bag directly because it is a bag-level scrub. Once v3 lands -// the query and raw-SQL arms carry no bag at all, so this narrows to routes on -// its own — via the migration, not via a guard here. +// Defensive hygiene over hand-written or externally-produced portable JSON, +// where a baked absolute window can appear in a params bag. +// +// v3 narrowed this on its own, exactly as the v2-era note here predicted: only +// the `route` arm still carries an untyped bag, so the scrub now applies to that +// arm and nothing else. The query and raw-SQL arms cannot smuggle a `startTime` +// through an opaque bag any more, because they no longer have one. function stripWidgetTimeParams(widget: DashboardWidget): DashboardWidget { - const params = widget.dataSource.params + const dataSource = widget.dataSource + if (dataSource.kind !== "route") return widget + const params = dataSource.params if (!params || !("startTime" in params || "endTime" in params)) return widget const { startTime: _startTime, endTime: _endTime, ...rest } = params - return { ...widget, dataSource: { ...widget.dataSource, params: rest } } + return { ...widget, dataSource: { ...dataSource, params: rest } } } export function parsePortableDashboardJson(json: string): PortableDashboard { diff --git a/apps/web/src/components/dashboard-builder/sections/section-layout.test.ts b/apps/web/src/components/dashboard-builder/sections/section-layout.test.ts index a23bdf2d6..50082b354 100644 --- a/apps/web/src/components/dashboard-builder/sections/section-layout.test.ts +++ b/apps/web/src/components/dashboard-builder/sections/section-layout.test.ts @@ -18,7 +18,7 @@ const widget = ( ): DashboardWidget => ({ id, visualization: "chart", - dataSource: { endpoint: "custom_query_builder_timeseries" }, + dataSource: { kind: "query", resultShape: "timeseries", queries: [] }, display: {}, layout, ...(membership ?? {}), diff --git a/apps/web/src/components/dashboard-builder/types.ts b/apps/web/src/components/dashboard-builder/types.ts index 8c9f002c2..c48e3a093 100644 --- a/apps/web/src/components/dashboard-builder/types.ts +++ b/apps/web/src/components/dashboard-builder/types.ts @@ -61,17 +61,26 @@ export type DataSourceEndpoint = | "raw_sql_chart" | "markdown_static" -// `endpoint` is narrowed to the registry key union so the data-source registry -// stays statically indexable; everything else comes straight from the schema. +// A straight alias of the schema type, as of v3. // -// Deliberately NOT `DeepMutable`, unlike the display/layout aliases below. A -// data source is replaced wholesale — the builder never edits one field of it in -// place — and keeping it readonly is what lets the constructors in -// `@maple/widgets/dashboard` (which return the schema type) be assigned here -// directly, instead of every call site fighting a variance mismatch. -export type WidgetDataSource = Omit & { - endpoint: DataSourceEndpoint -} +// This used to be `Omit<..., "endpoint"> & { endpoint: DataSourceEndpoint }`, +// which bundled two unrelated jobs: keeping `serverFunctionMap` statically total, +// and rejecting a typo'd endpoint at the widget-definition call site. Neither +// survives contact with a discriminated union — `Omit` over a union collapses to +// the keys every arm shares, so the result was a type that demanded `endpoint` +// from arms that do not have one. +// +// Both jobs now live where they belong: `DataSourceEndpoint` below is its own +// union and still keys the registry exhaustively, and the typo check moved to +// `makeRouteDataSource`'s type parameter in `@maple/widgets/dashboard`. +// +// Still deliberately NOT `DeepMutable`, unlike the display/layout aliases below. +// A data source is replaced wholesale — the builder never edits one field of it +// in place — and keeping it readonly is what lets the constructors in +// `@maple/widgets/dashboard` be assigned here directly, instead of every call +// site fighting a variance mismatch. `DeepMutable` over a union would also strip +// the readonly modifiers that make the arms discriminate cleanly. +export type WidgetDataSource = typeof WidgetDataSourceSchema.Type export type ValueUnit = | "none" diff --git a/apps/web/src/components/dashboard-builder/widgets/stat-widget.test.tsx b/apps/web/src/components/dashboard-builder/widgets/stat-widget.test.tsx index 956ff19d1..327ea5f0a 100644 --- a/apps/web/src/components/dashboard-builder/widgets/stat-widget.test.tsx +++ b/apps/web/src/components/dashboard-builder/widgets/stat-widget.test.tsx @@ -44,7 +44,7 @@ const displayWithSparkline: WidgetDisplayConfig = { title: "Requests", sparkline: { enabled: true, - dataSource: { endpoint: "custom_query_builder_timeseries", params: {} }, + dataSource: { kind: "query", resultShape: "timeseries", queries: [] }, }, } diff --git a/apps/web/src/components/dashboard-builder/widgets/types/markdown.tsx b/apps/web/src/components/dashboard-builder/widgets/types/markdown.tsx index 011fb87fc..6c5513078 100644 --- a/apps/web/src/components/dashboard-builder/widgets/types/markdown.tsx +++ b/apps/web/src/components/dashboard-builder/widgets/types/markdown.tsx @@ -9,7 +9,7 @@ import { } from "@/components/dashboard-builder/widgets/widget-type-registry" import { PreviewFrame } from "@/components/dashboard-builder/widgets/types/preset-preview" import type { WidgetPresetDefinition } from "@/components/dashboard-builder/widgets/widget-definitions" -import { makeRouteDataSource } from "@maple/widgets/dashboard" +import { makeStaticDataSource } from "@maple/widgets/dashboard" /** The note's first few lines, with markdown syntax stripped. */ function MarkdownPresetPreview({ preset }: { preset: WidgetPresetDefinition }) { @@ -46,7 +46,7 @@ export const markdownWidgetType: WidgetTypeDefinition = { initialState: (widget) => ({ markdownContent: widget.display.markdown?.content ?? "" }), - buildDataSource: () => makeRouteDataSource("markdown_static"), + buildDataSource: () => makeStaticDataSource(), buildDisplay: ({ base, state }) => extendDisplay(base, { markdown: { content: state.markdownContent } }), } diff --git a/apps/web/src/components/dashboard-builder/widgets/widget-definitions.test.ts b/apps/web/src/components/dashboard-builder/widgets/widget-definitions.test.ts index d993b8ae8..8bf3e1608 100644 --- a/apps/web/src/components/dashboard-builder/widgets/widget-definitions.test.ts +++ b/apps/web/src/components/dashboard-builder/widgets/widget-definitions.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest" +import type { QueryResultShape } from "@maple/query-model" import type { QueryBuilderQueryDraftPayload } from "@maple/domain/http" import { buildBreakdownQuerySpec, buildListQuerySpec } from "@maple/query-engine/query-builder" import { @@ -30,14 +31,20 @@ const allPresets: WidgetPresetDefinition[] = [ ] function presetQueries(preset: WidgetPresetDefinition): QueryBuilderQueryDraftPayload[] { - const params = preset.dataSource.params as { queries?: QueryBuilderQueryDraftPayload[] } | undefined - return params?.queries ?? [] + const dataSource = preset.dataSource + return dataSource.kind === "query" ? [...dataSource.queries] : [] } +/** + * v3 replaced the `custom_query_builder_*` endpoint names with `kind: "query"` + * plus a `resultShape`, so the presets are selected by shape here rather than by + * endpoint string. + */ +const hasShape = (preset: WidgetPresetDefinition, shape: QueryResultShape): boolean => + preset.dataSource.kind === "query" && preset.dataSource.resultShape === shape + describe("widget preset query specs", () => { - for (const preset of allPresets.filter( - (p) => p.dataSource.endpoint === "custom_query_builder_breakdown", - )) { + for (const preset of allPresets.filter((p) => hasShape(p, "breakdown"))) { it(`${preset.id} builds a valid breakdown spec for every query`, () => { const queries = presetQueries(preset) expect(queries.length).toBeGreaterThan(0) @@ -49,7 +56,7 @@ describe("widget preset query specs", () => { }) } - for (const preset of allPresets.filter((p) => p.dataSource.endpoint === "custom_query_builder_list")) { + for (const preset of allPresets.filter((p) => hasShape(p, "list"))) { it(`${preset.id} builds a valid list spec`, () => { const queries = presetQueries(preset) expect(queries.length).toBeGreaterThan(0) @@ -73,7 +80,7 @@ describe("widget preset query specs", () => { it("every horizontal-bar preset groups by a category", () => { expect(hbarPresets.length).toBeGreaterThan(0) for (const preset of hbarPresets) { - expect(preset.dataSource.endpoint, preset.id).toBe("custom_query_builder_breakdown") + expect(hasShape(preset, "breakdown"), preset.id).toBe(true) for (const query of presetQueries(preset)) { expect(query.addOns?.groupBy, preset.id).toBe(true) expect(query.groupBy?.length ?? 0, preset.id).toBeGreaterThan(0) @@ -84,8 +91,10 @@ describe("widget preset query specs", () => { it("histogram duration preset queries raw durations, not a category breakdown", () => { const histogram = histogramPresets.find((p) => p.id === "histogram-trace-duration") expect(histogram).toBeDefined() - expect(histogram!.dataSource.endpoint).toBe("custom_query_builder_list") - expect((histogram!.dataSource.params as { columns?: string[] }).columns).toEqual(["durationMs"]) + const source = histogram!.dataSource + if (source.kind !== "query") throw new Error("expected a query data source") + expect(source.resultShape).toBe("list") + expect(source.columns).toEqual(["durationMs"]) expect(histogram!.display.unit).toBe("duration_ms") }) }) diff --git a/apps/web/src/components/dashboard-builder/widgets/widget-definitions.ts b/apps/web/src/components/dashboard-builder/widgets/widget-definitions.ts index 89209199e..ac80cd59c 100644 --- a/apps/web/src/components/dashboard-builder/widgets/widget-definitions.ts +++ b/apps/web/src/components/dashboard-builder/widgets/widget-definitions.ts @@ -15,7 +15,7 @@ import type { WidgetDataSource, WidgetDisplayConfig, } from "@/components/dashboard-builder/types" -import { makeQueryDataSource, makeRouteDataSource } from "@maple/widgets/dashboard" +import { makeQueryDataSource, makeRouteDataSource, makeStaticDataSource } from "@maple/widgets/dashboard" export interface WidgetPresetDefinition { id: string @@ -585,7 +585,7 @@ export const markdownPresets: WidgetPresetDefinition[] = [ description: "Static markdown note for context, links, or runbooks", icon: FileIcon, visualization: "markdown", - dataSource: makeRouteDataSource("markdown_static"), + dataSource: makeStaticDataSource(), display: { title: "Note", markdown: { diff --git a/apps/web/src/components/layout/app-update-banner.tsx b/apps/web/src/components/layout/app-update-banner.tsx new file mode 100644 index 000000000..ff12cfcc2 --- /dev/null +++ b/apps/web/src/components/layout/app-update-banner.tsx @@ -0,0 +1,48 @@ +import { Alert, AlertAction, AlertDescription, AlertTitle } from "@maple/ui/components/ui/alert" +import { Button } from "@maple/ui/components/ui/button" + +import { ArrowRotateClockwiseIcon } from "@/components/icons" +import { useAppVersionChanged } from "@/hooks/use-app-version" + +// Dev-only escape hatch, mirroring `QuotaBanner`'s: load any page with +// `?version_preview=1` to eyeball the banner without deploying twice. Compiled +// out of production builds. +function previewRequested(): boolean { + if (!import.meta.env.DEV || typeof window === "undefined") return false + return new URLSearchParams(window.location.search).get("version_preview") === "1" +} + +/** + * Shown when the server is serving a newer build than this tab is running. + * + * Deliberately NOT dismissible. Every other banner in the shell is advisory — + * this one means the code in this tab is out of date, which during a stored-schema + * rollout can mean it cannot read documents the current build writes. A tab that + * dismissed it would go on silently failing to decode dashboards, and the user + * would have no way back to the prompt that explains why. + * + * The copy matches `StaleChunkError` in `lib/error-messages.ts` word for word. + * Same event from the user's side — Maple was updated, reload to catch up — and + * they should not have to work out that the banner and the error screen are + * talking about the same thing. + */ +export function AppUpdateBanner() { + const changed = useAppVersionChanged() + + if (!changed && !previewRequested()) return null + + return ( +
+ + + Maple was updated + Reload to use the latest version. + + + + +
+ ) +} diff --git a/apps/web/src/components/layout/dashboard-layout.tsx b/apps/web/src/components/layout/dashboard-layout.tsx index c1c44ad31..e2564e47b 100644 --- a/apps/web/src/components/layout/dashboard-layout.tsx +++ b/apps/web/src/components/layout/dashboard-layout.tsx @@ -20,6 +20,7 @@ import { openGlobalChat } from "@/components/chat/global-chat-sheet" import { ConnectButton } from "@/components/header/connect-button" import { QuotaBanner } from "@/components/billing/quota-banner" import { PaymentFailedBanner } from "@/components/billing/payment-failed-banner" +import { AppUpdateBanner } from "@/components/layout/app-update-banner" import { Link, defaultParseSearch } from "@tanstack/react-router" import { isClerkAuthEnabled } from "@/lib/services/common/auth-mode" @@ -155,10 +156,14 @@ function Breadcrumbs({ items, children }: { items: BreadcrumbEntry[]; children?: ) } -/** Billing banners + the horizontal `Filters | Content | RightPanel` row. */ +/** App-shell banners + the horizontal `Filters | Content | RightPanel` row. */ function Body({ children }: { children: React.ReactNode }) { return ( <> + {/* Ungated, unlike the billing banners below: a stale bundle is stale + whether or not the deployment uses Clerk, and self-hosted installs + have the same long-lived-tab problem. */} + {isClerkAuthEnabled && } {isClerkAuthEnabled && } {children} diff --git a/apps/web/src/hooks/use-app-version.ts b/apps/web/src/hooks/use-app-version.ts new file mode 100644 index 000000000..03c094900 --- /dev/null +++ b/apps/web/src/hooks/use-app-version.ts @@ -0,0 +1,98 @@ +import * as React from "react" + +import { useMountEffect } from "@/hooks/use-mount-effect" + +/** Slow on purpose — a deploy the user learns about 15 minutes late costs nothing. */ +const POLL_INTERVAL_MS = 15 * 60 * 1000 + +/** + * The commit this bundle was built from, baked in by Vite's `define`. + * + * Empty for any build that is not a deploy (local dev, tests, previews), which + * is the switch that makes this whole check inert there. + */ +const BUILT_COMMIT: string = import.meta.env.VITE_COMMIT_SHA ?? "" + +const fetchDeployedCommit = async (): Promise => { + try { + // Cache-busted twice over. `no-store` handles the browser's HTTP cache and + // the query param handles any edge or proxy cache that keys on URL alone — + // a poll answered from cache reports the deploy this tab is already running, + // which is precisely the failure this function exists to avoid. + const response = await fetch(`/version.json?t=${Date.now()}`, { cache: "no-store" }) + if (!response.ok) return null + const body: unknown = await response.json() + if (typeof body !== "object" || body === null) return null + const commit = (body as { commit?: unknown }).commit + return typeof commit === "string" && commit.length > 0 ? commit : null + } catch { + // Offline, or a deploy swapping assets underneath us. Either way this is a + // best-effort background probe: staying quiet and trying again on the next + // tick is strictly better than surfacing a network error for something the + // user never asked for. + return null + } +} + +/** + * True once the server is serving a different build than this tab is running. + * + * Exists because a long-lived tab has no other reason to reload. The app already + * recovers from a *stale chunk* (`lib/chunk-reload.ts`), but only reactively — + * after a navigation has already failed. That leaves a tab that never navigates + * running arbitrarily old JS against a moving API, which is a real correctness + * problem during a stored-schema rollout: `apps/web/src/lib/collections/dashboards.ts` + * migrates dashboard documents client-side, so a tab whose bundle predates a + * schema version cannot read documents written in it. + * + * Never auto-reloads. A dashboard being edited has unsaved state in memory, and + * silently discarding it to save the user one click is a bad trade. The banner + * asks; the user picks the moment. + * + * Latches: once a newer deploy is seen it stays seen, even if a subsequent poll + * fails or a rollback restores the original commit. The tab has already been + * told it is behind, and flickering the banner away would be worse than leaving + * a reload prompt up for a build that is once again current. + */ +export function useAppVersionChanged(): boolean { + const [changed, setChanged] = React.useState(false) + + useMountEffect(() => { + if (BUILT_COMMIT.length === 0) return + + let cancelled = false + let timeout: ReturnType | undefined + + const probe = async () => { + if (cancelled || document.hidden) return + const deployed = await fetchDeployedCommit() + if (cancelled || deployed === null) return + if (deployed !== BUILT_COMMIT) setChanged(true) + } + + const schedule = () => { + timeout = setTimeout(() => { + void probe() + schedule() + }, POLL_INTERVAL_MS) + } + + // The event that actually matters. The tab this is written for has been + // hidden for hours or days; it learns it is stale the moment someone looks + // at it, not up to a poll interval later. + const onVisibilityChange = () => { + if (!document.hidden) void probe() + } + + document.addEventListener("visibilitychange", onVisibilityChange) + schedule() + + return () => { + cancelled = true + document.removeEventListener("visibilitychange", onVisibilityChange) + if (timeout !== undefined) clearTimeout(timeout) + } + }) + + return changed +} diff --git a/apps/web/src/hooks/use-dashboard-store.test.tsx b/apps/web/src/hooks/use-dashboard-store.test.tsx index 9479fe889..0c3c25d3a 100644 --- a/apps/web/src/hooks/use-dashboard-store.test.tsx +++ b/apps/web/src/hooks/use-dashboard-store.test.tsx @@ -81,8 +81,9 @@ const makeRow = (id: string, widgets: ReadonlyArray = []): DashboardRow }) const chartDataSource: WidgetDataSource = { - endpoint: "custom_query_builder_timeseries", - params: { queries: [], formulas: [] }, + kind: "query", + resultShape: "timeseries", + queries: [], } const widgetsOf = (id: string) => diff --git a/apps/web/src/hooks/use-widget-data.ts b/apps/web/src/hooks/use-widget-data.ts index fcca9f649..4d5309906 100644 --- a/apps/web/src/hooks/use-widget-data.ts +++ b/apps/web/src/hooks/use-widget-data.ts @@ -12,15 +12,16 @@ import { hasUnresolvedVariableRefs, interpolateWidgetParams } from "@maple/query import type { DashboardWidget, TimeRange, WidgetDataSource } from "@/components/dashboard-builder/types" /** - * The structural shape a data source must satisfy to be fetched. Both the - * web `WidgetDataSource` and the JSON-decoded `display.sparkline.dataSource` - * (whose `endpoint` is only typed as `string`) are assignable to this. + * Was a structural stand-in, back when a widget's own data source narrowed + * `endpoint` to the registry key union while the JSON-decoded + * `display.sparkline.dataSource` typed it as a bare `string` — so neither was + * assignable to the other and both had to be assignable to a third thing. + * + * v3 removed the discrepancy: both are the same union now. Kept as an alias only + * so the sparkline call sites keep reading as "any data source, not necessarily + * this widget's own". */ -export type WidgetDataSourceLike = { - endpoint: string - params?: Record - transform?: WidgetDataSource["transform"] -} +export type WidgetDataSourceLike = WidgetDataSource import { disabledResultAtom } from "@/lib/services/atoms/disabled-result-atom" import { MapleApiAtomClient } from "@/lib/services/common/atom-client" import type { WidgetDataState } from "@/components/dashboard-builder/types" diff --git a/apps/web/src/lib/dashboards/section-view-state.test.ts b/apps/web/src/lib/dashboards/section-view-state.test.ts index 436a8626e..db780fa9b 100644 --- a/apps/web/src/lib/dashboards/section-view-state.test.ts +++ b/apps/web/src/lib/dashboards/section-view-state.test.ts @@ -27,7 +27,7 @@ const section = ( const widget = (id: string, membership?: { sectionId: string; tabId: string }): DashboardWidget => ({ id, visualization: "chart", - dataSource: { endpoint: "custom_query_builder_timeseries" }, + dataSource: { kind: "query", resultShape: "timeseries", queries: [] }, display: {}, layout: { x: 0, y: 0, w: 4, h: 4 }, ...(membership ?? {}), @@ -178,10 +178,7 @@ describe("resolveSectionView", () => { // because a deleted key isn't present to overwrite the stale value. describe("route composition", () => { // Mirrors `applySectionView` in routes/dashboards/$dashboardId.tsx. - const apply = ( - prev: Record, - update: (p: SectionViewSearch) => SectionViewSearch, - ) => ({ + const apply = (prev: Record, update: (p: SectionViewSearch) => SectionViewSearch) => ({ ...update(pickDashboardControlParams(prev)), ...(prev.mode === "edit" ? { mode: "edit" as const } : {}), }) @@ -205,9 +202,7 @@ describe("route composition", () => { }) it("keeps edit mode and variable selections across a collapse", () => { - const next = apply({ mode: "edit", "var-service": "api" }, (p) => - withSectionCollapsed(p, "s1", true), - ) + const next = apply({ mode: "edit", "var-service": "api" }, (p) => withSectionCollapsed(p, "s1", true)) expect(next).toEqual({ mode: "edit", "var-service": "api", collapsed: "s1" }) }) diff --git a/apps/web/src/lib/models/dashboards-list-model.test.ts b/apps/web/src/lib/models/dashboards-list-model.test.ts index 5aba4dbed..796af52df 100644 --- a/apps/web/src/lib/models/dashboards-list-model.test.ts +++ b/apps/web/src/lib/models/dashboards-list-model.test.ts @@ -60,9 +60,11 @@ describe("deriveDashboardsList", () => { widgets: [ { id: "w1", - // A panel type in `visualization` — the shape v1 allowed. + // A panel type in `visualization` — the shape v1 allowed. The + // migration chain still folds this on read; only the data source + // moved to the one-shot backfill, so it is written in v3 here. visualization: "bar", - dataSource: { endpoint: "spanMetrics" }, + dataSource: { kind: "route", endpoint: "spanMetrics" }, display: { title: "Errors" }, layout: { x: 0, y: 0, w: 4, h: 5 }, }, @@ -98,7 +100,7 @@ describe("deriveDashboardsList", () => { { id: "w1", visualization: "chart", - dataSource: { endpoint: "spanMetrics" }, + dataSource: { kind: "route", endpoint: "spanMetrics" }, display: { title: "p95" }, layout: { x: 0, y: 0, w: 4, h: 5 }, }, diff --git a/apps/web/src/lib/query-builder/widget-builder-utils.test.ts b/apps/web/src/lib/query-builder/widget-builder-utils.test.ts index eda3c1f0b..c6b2e014a 100644 --- a/apps/web/src/lib/query-builder/widget-builder-utils.test.ts +++ b/apps/web/src/lib/query-builder/widget-builder-utils.test.ts @@ -1,3 +1,4 @@ +import type { WidgetDataSource } from "@/components/dashboard-builder/types" import { describe, expect, it } from "vitest" import { BREAKDOWN_TAIL_LIMIT, createFormulaDraft, createQueryDraft } from "@maple/query-engine/query-builder" import { @@ -12,13 +13,31 @@ import { } from "@/lib/query-builder/widget-builder-utils" import type { DashboardWidget } from "@/components/dashboard-builder/types" +/** + * What a widget routed to, as one comparable value. + * + * v2 answered this with an endpoint string; v3 answers it with `kind` plus, for + * queries, `resultShape`. Collapsing the two back into a single token keeps these + * routing assertions reading as routing assertions rather than as narrowing. + */ +const routedTo = (dataSource: WidgetDataSource): string => + dataSource.kind === "query" ? dataSource.resultShape : dataSource.kind + +/** The query arm's request-shaping fields, which v2 kept in the `params` bag. */ +const queryFields = (dataSource: WidgetDataSource): Record => { + if (dataSource.kind !== "query") throw new Error(`expected a query source, got ${dataSource.kind}`) + const { kind: _kind, resultShape: _shape, transform: _transform, ...fields } = dataSource + return fields +} + function makeWidget(): DashboardWidget { return { id: "widget-1", visualization: "chart", dataSource: { - endpoint: "custom_query_builder_timeseries", - params: {}, + kind: "query", + resultShape: "timeseries", + queries: [], }, display: {}, layout: { x: 0, y: 0, w: 6, h: 4 }, @@ -79,7 +98,7 @@ describe("widget-builder hidden series behavior", () => { const dataSource = buildWidgetDataSource(widget, state, ["A", "B"]) - expect(dataSource.endpoint).toBe("custom_query_builder_timeseries") + expect(routedTo(dataSource)).toBe("timeseries") expect(dataSource.transform?.hideSeries?.baseNames).toEqual(["A"]) }) @@ -196,7 +215,7 @@ describe("widget-builder hidden series behavior", () => { const dataSource = buildWidgetDataSource(widget, state, ["B"]) expect(dataSource.transform?.hideSeries?.baseNames).toEqual(["Errors", "Error ratio"]) - expect(dataSource.params).toMatchObject({ + expect(queryFields(dataSource)).toMatchObject({ queries: state.queries, formulas: state.formulas, }) @@ -210,14 +229,14 @@ describe("funnel/heatmap endpoint routing (MAP-49)", () => { const widget = makeWidget() const state = { ...makeState(), visualization } const dataSource = buildWidgetDataSource(widget, state, ["A", "B"]) - expect(dataSource.endpoint).toBe("custom_query_builder_breakdown") + expect(routedTo(dataSource)).toBe("breakdown") }, ) it("keeps charts on the timeseries endpoint", () => { const widget = makeWidget() const dataSource = buildWidgetDataSource(widget, makeState(), ["A", "B"]) - expect(dataSource.endpoint).toBe("custom_query_builder_timeseries") + expect(routedTo(dataSource)).toBe("timeseries") }) it("sends breakdown params the endpoint schema accepts, and nothing more", () => { @@ -231,7 +250,7 @@ describe("funnel/heatmap endpoint routing (MAP-49)", () => { formulas: [createFormulaDraft(0, ["A", "B"])], } const dataSource = buildWidgetDataSource(makeWidget(), state, ["A", "B"]) - expect(Object.keys(dataSource.params ?? {})).toEqual(["queries", "defaultLimit"]) + expect(Object.keys(queryFields(dataSource))).toEqual(["queries", "defaultLimit"]) }) it("asks for the long tail on a pie, and only on a pie", () => { @@ -242,14 +261,14 @@ describe("funnel/heatmap endpoint routing (MAP-49)", () => { "A", "B", ]) - expect(pie.params?.defaultLimit).toBe(BREAKDOWN_TAIL_LIMIT) + expect(queryFields(pie).defaultLimit).toBe(BREAKDOWN_TAIL_LIMIT) for (const visualization of ["funnel", "heatmap", "histogram"] as const) { const dataSource = buildWidgetDataSource(makeWidget(), { ...makeState(), visualization }, [ "A", "B", ]) - expect(dataSource.params?.defaultLimit).toBeUndefined() + expect(queryFields(dataSource).defaultLimit).toBeUndefined() } }) }) @@ -268,15 +287,13 @@ describe("histogram data shape routing", () => { // An ungrouped histogram is a distribution of raw values bucketized // client-side — a count-by-group breakdown is a different chart (MAP-49). const dataSource = buildWidgetDataSource(makeWidget(), ungroupedTraceState(), ["A"]) - expect(dataSource.endpoint).toBe("custom_query_builder_list") - expect(dataSource.params).toMatchObject({ columns: ["durationMs"] }) + expect(routedTo(dataSource)).toBe("list") + expect(queryFields(dataSource)).toMatchObject({ columns: ["durationMs"] }) }) it("routes a grouped histogram to the breakdown endpoint", () => { const state = { ...makeState(), visualization: "histogram" as const } - expect(buildWidgetDataSource(makeWidget(), state, ["A"]).endpoint).toBe( - "custom_query_builder_breakdown", - ) + expect(routedTo(buildWidgetDataSource(makeWidget(), state, ["A"]))).toBe("breakdown") }) it("round-trips a list-backed histogram instead of dropping its query", () => { @@ -336,7 +353,7 @@ describe("markdown widgets", () => { const dataSource = buildWidgetDataSource(makeWidget(), state, []) const display = buildWidgetDisplay(makeWidget(), state) - expect(dataSource.endpoint).toBe("markdown_static") + expect(routedTo(dataSource)).toBe("static") expect(display.markdown).toEqual({ content: "# Runbook" }) expect( toInitialState({ ...makeWidget(), visualization: "markdown", dataSource, display }) diff --git a/apps/web/src/lib/query-builder/widget-type-cycle.test.ts b/apps/web/src/lib/query-builder/widget-type-cycle.test.ts index bd424959e..ccaaa6554 100644 --- a/apps/web/src/lib/query-builder/widget-type-cycle.test.ts +++ b/apps/web/src/lib/query-builder/widget-type-cycle.test.ts @@ -12,6 +12,16 @@ import { } from "@/lib/query-builder/widget-builder-utils" import type { DashboardWidget } from "@/components/dashboard-builder/types" +/** + * What a widget routed to, as one comparable value. + * + * v2 answered this with an endpoint string; v3 answers it with `kind` plus, for + * queries, `resultShape`. Collapsing them back into a single token keeps these + * routing assertions reading as routing assertions rather than as narrowing. + */ +const routedTo = (dataSource: DashboardWidget["dataSource"]): string => + dataSource.kind === "query" ? dataSource.resultShape : dataSource.kind + // Switching a widget's panel type, exhaustively. // // The editor's Type picker lets any widget become any other, and the lowering @@ -85,7 +95,7 @@ function makeWidget(overrides: Partial = {}): DashboardWidget { return { id: "widget-1", visualization: "chart", - dataSource: { endpoint: "custom_query_builder_timeseries", params: {} }, + dataSource: { kind: "query", resultShape: "timeseries", queries: [] }, display: {}, layout: { x: 0, y: 0, w: 6, h: 4 }, ...overrides, @@ -163,10 +173,11 @@ describe("endpoint routing", () => { it("sends the categorical types to the breakdown endpoint", () => { for (const panel of ["pie", "funnel", "heatmap", "hbar"] as const) { const source = buildWidgetDataSource(makeWidget(), selectPanel(makeState(), panel), ["A"]) - expect(source.endpoint, panel).toBe("custom_query_builder_breakdown") + expect(routedTo(source), panel).toBe("breakdown") // A formula is a timeseries expression; the breakdown input schema // rejects it, and the widget then hangs on its loading skeleton. - expect((source.params as Record).formulas, panel).toBeUndefined() + if (source.kind !== "query") throw new Error("expected a query data source") + expect(source.formulas, panel).toBeUndefined() } }) @@ -175,16 +186,17 @@ describe("endpoint routing", () => { // is the one type whose endpoint depends on its queries, not just its kind. const ungrouped = makeState({ queries: [ungroupedQuery()] }) const listSource = buildWidgetDataSource(makeWidget(), selectPanel(ungrouped, "histogram"), ["A"]) - expect(listSource.endpoint).toBe("custom_query_builder_list") - expect((listSource.params as Record).columns).toEqual(["durationMs"]) + expect(routedTo(listSource)).toBe("list") + if (listSource.kind !== "query") throw new Error("expected a query data source") + expect(listSource.columns).toEqual(["durationMs"]) const grouped = buildWidgetDataSource(makeWidget(), selectPanel(makeState(), "histogram"), ["A"]) - expect(grouped.endpoint).toBe("custom_query_builder_breakdown") + expect(routedTo(grouped)).toBe("breakdown") }) it("gives a note no query at all", () => { const source = buildWidgetDataSource(makeWidget(), selectPanel(makeState(), "markdown"), ["A"]) - expect(source).toEqual({ endpoint: "markdown_static" }) + expect(source).toEqual({ kind: "static" }) }) it("reduces stat and gauge to a scalar", () => { @@ -206,7 +218,7 @@ describe("stat sparkline", () => { expect(display.sparkline?.enabled).toBe(true) const nested = display.sparkline?.dataSource - expect(nested?.endpoint).toBe("custom_query_builder_timeseries") + expect(nested === undefined ? undefined : routedTo(nested)).toBe("timeseries") // The sparkline is a trend, so it must NOT carry the stat's reduction. expect(nested?.transform?.reduceToValue).toBeUndefined() }) diff --git a/apps/web/src/worker.ts b/apps/web/src/worker.ts index 87252168c..7ecc7ba91 100644 --- a/apps/web/src/worker.ts +++ b/apps/web/src/worker.ts @@ -8,6 +8,15 @@ export default { const assetResponse = await env.ASSETS.fetch(request) if (assetResponse.status !== 404) { + // `/version.json` is the one asset whose whole job is to be stale-free: + // clients poll it to learn a newer bundle is deployed. Served from any + // cache it would report the deploy the tab is already running, which is + // exactly the answer that makes the check useless. + if (url.pathname === "/version.json") { + const response = new Response(assetResponse.body, assetResponse) + response.headers.set("Cache-Control", "no-store, must-revalidate") + return response + } return assetResponse } diff --git a/apps/web/vite-plugin-version-manifest.ts b/apps/web/vite-plugin-version-manifest.ts new file mode 100644 index 000000000..094294f3d --- /dev/null +++ b/apps/web/vite-plugin-version-manifest.ts @@ -0,0 +1,32 @@ +import type { Plugin } from "vite" + +/** + * Emits `/version.json` carrying the commit this bundle was built from. + * + * The bundle already knows its own commit — `import.meta.env.VITE_COMMIT_SHA` is + * baked in at build time for telemetry. What it has no way to know is what the + * *server* is currently serving, and that gap is why a tab left open across a + * deploy keeps running old JS indefinitely: nothing tells it otherwise until it + * happens to request a hashed chunk that no longer exists. + * + * A one-line static asset closes it. The client polls this file and compares it + * against its own baked-in value; a mismatch means a newer bundle is deployed. + * + * Deliberately a build artifact rather than a runtime endpoint: it is served by + * the same assets layer as the bundle it describes, so the two can never + * disagree about which deploy is live — which a separately-deployed `/version` + * route absolutely could, mid-rollout. + */ +export function versionManifest(commitSha: string): Plugin { + return { + name: "maple:version-manifest", + apply: "build", + generateBundle() { + this.emitFile({ + type: "asset", + fileName: "version.json", + source: JSON.stringify({ commit: commitSha }), + }) + }, + } +} diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index f7448686f..c610b74e1 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -6,6 +6,7 @@ import tanstackRouter from "@tanstack/router-plugin/vite" import viteReact from "@vitejs/plugin-react" import tailwindcss from "@tailwindcss/vite" import { siblingUrl } from "../../packages/infra/src/dev-urls.ts" +import { versionManifest } from "./vite-plugin-version-manifest.ts" const envDir = path.resolve(import.meta.dirname, "../..") @@ -97,6 +98,11 @@ export default defineConfig(({ mode }) => { }), tailwindcss(), viteReact(), + // Reads the same `process.env` the `define` block above does, rather than + // `import.meta.env`, because this runs in the Vite process and not in the + // bundle. An empty value (any build that is not a deploy) makes the + // client-side check inert — see `use-app-version.ts`. + versionManifest(process.env.VITE_COMMIT_SHA?.trim() || ""), ], build: { // The bundle budget reads Vite's static/dynamic import graph instead of diff --git a/packages/domain/src/dashboard-variables.test.ts b/packages/domain/src/dashboard-variables.test.ts index 255c25225..75c9d64c9 100644 --- a/packages/domain/src/dashboard-variables.test.ts +++ b/packages/domain/src/dashboard-variables.test.ts @@ -91,7 +91,7 @@ describe("DashboardDocument with variables", () => { const widget = (id: string, membership: Record = {}) => ({ id, visualization: "chart", - dataSource: { endpoint: "custom_query_builder_timeseries" }, + dataSource: { kind: "query", resultShape: "timeseries", queries: [] }, display: {}, layout: { x: 0, y: 0, w: 4, h: 4 }, ...membership, diff --git a/packages/domain/src/http/v2/dashboard-widget-parity.test.ts b/packages/domain/src/http/v2/dashboard-widget-parity.test.ts index d5a487233..3d13bde6d 100644 --- a/packages/domain/src/http/v2/dashboard-widget-parity.test.ts +++ b/packages/domain/src/http/v2/dashboard-widget-parity.test.ts @@ -1,7 +1,8 @@ import { Schema } from "effect" import { describe, expect, it } from "vitest" +import { WIDGET_DATA_SOURCE_KINDS } from "@maple/widgets/dashboard" import { DashboardWidgetSchema, WidgetDisplayConfigSchema } from "../dashboards" -import { V2DashboardWidget, V2WidgetDisplay } from "./dashboards" +import { V2DashboardWidget, V2WidgetDataSource, V2WidgetDisplay } from "./dashboards" // The v2 public schema is a hand-maintained clone of the stored widget schema: // the same fields again, differing only by `Schema.encodeKeys` for snake_case. @@ -20,31 +21,26 @@ const snakeCase = (key: string): string => key.replace(/[A-Z]/g, (c) => `_${c.to /** Fails to compile unless `A` is assignable to `B`. */ type AssertAssignable = [A, B] -// Bidirectional: a field added to EITHER schema and missed on the other breaks -// the build here rather than silently changing the public API. -export type _DisplayInternalToV2 = AssertAssignable< - typeof WidgetDisplayConfigSchema.Type, - typeof V2WidgetDisplay.Type -> -export type _DisplayV2ToInternal = AssertAssignable< - typeof V2WidgetDisplay.Type, - typeof WidgetDisplayConfigSchema.Type -> -// The widget is asserted in one direction only, and deliberately. +// The three structural assignability assertions that used to live here are gone, +// and deliberately. // -// This is the direction the silent-omission bug actually travels: -// `routes/v2/dashboards.http.ts` assigns `widgets: dashboard.widgets` — internal -// widgets into the V2 type — so internal must satisfy V2. It still catches a -// field added to V2 alone, because a new *required* V2 field would leave internal -// no longer assignable. +// They asserted that the stored widget/display types were mutually assignable +// with the v2 ones — which held only while the v2 schemas were a field-for-field +// clone of storage. Schema v3 ends that: `dataSource.queries` is a typed array of +// query drafts in storage and an opaque `UnknownRecord` on the wire, because the +// wire representation of a query has always been opaque (it lived inside the +// untyped `params` bag) and typing it now would be a second, larger breaking +// change to the published spec. // -// The reverse does not hold, and forcing it would be wrong: V2 types an absolute -// `timeRange` as a plain `Timestamp` string while the stored schema brands it -// `IsoDateTimeString`. `toInternalWidgets` in the route re-brands it on the way -// in — which is precisely why that function exists. -export type _WidgetInternalToV2 = AssertAssignable< - typeof DashboardWidgetSchema.Type, - typeof V2DashboardWidget.Type +// So assignability is genuinely false, and forcing it would mean either leaking +// storage types into the public contract or weakening storage to match the wire. +// What the assertions actually PROTECTED — "you cannot extend the internal schema +// without deciding what the public API does" — is preserved by the kind-coverage +// test at the bottom of this file, which fails when an arm is added to the stored +// union and not mirrored here. +export type _DisplayFieldsCovered = AssertAssignable< + keyof typeof V2WidgetDisplay.Type, + keyof typeof WidgetDisplayConfigSchema.Type > /** @@ -87,7 +83,7 @@ const fullDisplay = { thresholds: [{ value: 1, color: "#f00", label: "warn" }], prefix: "~", suffix: "/s", - sparkline: { enabled: true, dataSource: { endpoint: "custom_query_builder_timeseries" } }, + sparkline: { enabled: true, dataSource: { kind: "route", endpoint: "list_traces" } }, columns: [ { field: "name", @@ -144,7 +140,7 @@ describe("the v2 widget display mirrors the stored one", () => { histogram: { bucket_count: 10, bucket_width: 2, log_scale_y: false }, heatmap: { color_scale: "amber", scale_type: "linear" }, funnel: { show_step_percent: true }, - sparkline: { data_source: { endpoint: "custom_query_builder_timeseries" } }, + sparkline: { data_source: { kind: "route", endpoint: "list_traces" } }, list_where_clause: "service.name = $service", list_root_only: true, }) @@ -166,8 +162,9 @@ describe("the v2 widget mirrors the stored one", () => { id: "w1", visualization: "chart", dataSource: { - endpoint: "custom_query_builder_timeseries", - params: { queries: [] }, + kind: "query", + resultShape: "timeseries", + queries: [], transform: { fieldMap: { a: "b" }, hideSeries: { baseNames: ["x"] }, @@ -215,3 +212,35 @@ describe("the v2 widget mirrors the stored one", () => { }) }) }) + +// The structural successor to the deleted assignability assertions. +// +// A fifth arm added to the stored data-source union fails here until someone has +// decided what `/v2/dashboards` does with it — which is the question the old type +// assertions forced, and the only one worth forcing. +describe("the v2 data source covers every stored kind", () => { + const decode = Schema.decodeUnknownSync(V2WidgetDataSource) + + it.each(WIDGET_DATA_SOURCE_KINDS)("declares an arm for kind %s", (kind) => { + const fixture: Record = { + query: { kind: "query", result_shape: "timeseries", queries: [] }, + raw_sql: { kind: "raw_sql", sql: "SELECT 1" }, + route: { kind: "route", endpoint: "list_traces" }, + static: { kind: "static" }, + }[kind]! + + expect(() => decode(fixture)).not.toThrow() + }) + + it("snake_cases the scalar fields the union added", () => { + const wire = Schema.encodeUnknownSync(V2WidgetDataSource)({ + kind: "query", + resultShape: "breakdown", + queries: [], + defaultLimit: 20, + }) as Record + + expect(wire).toHaveProperty("result_shape", "breakdown") + expect(wire).toHaveProperty("default_limit", 20) + }) +}) diff --git a/packages/domain/src/http/v2/dashboards.ts b/packages/domain/src/http/v2/dashboards.ts index 1268326f6..06d511f37 100644 --- a/packages/domain/src/http/v2/dashboards.ts +++ b/packages/domain/src/http/v2/dashboards.ts @@ -24,6 +24,12 @@ import { DashboardVersionChangeKind, } from "../dashboards" import { SORT_DIRECTIONS, STAT_AGGREGATES } from "@maple/widgets/dashboard" +import { + QUERY_RESULT_SHAPES, + QueryBuilderFormulaSchema, + QueryBuilderQueryDraftSchema, + QueryComparisonSchema, +} from "@maple/query-model" import { HEATMAP_COLOR_SCALES, HEATMAP_SCALE_TYPES, WIDGET_VISUALIZATIONS } from "../widget-types" import { AuthorizationV2 } from "./auth" import { ListOf, ListQuery, Timestamp } from "./envelopes" @@ -70,6 +76,31 @@ const UnknownRecord = UnknownRecordWire.pipe( }), ) +/** + * The same recursive snake_case convention, but decoding into a TYPED schema + * instead of an opaque record. + * + * Needed by the v3 data source. A query draft used to travel inside the untyped + * `params` bag, so its fields were snake_cased by `UnknownRecord` above and never + * validated. v3 hoists `queries` to a real field, which leaves two bad options + * and one good one: keep it opaque (the wire is preserved but the route cannot + * assign it into the stored type without a second decode), type it directly (the + * route works but every draft field silently renames on the published wire), or + * this — snake_case on the wire, typed after decode. + * + * The wire bytes are therefore identical to v2 while the decoded value is the + * real draft, which is what lets `toInternalWidgets` stay a plain field carry. + */ +const snakeCasedWire = (schema: S) => + UnknownRecordWire.pipe( + Schema.decodeTo(schema, { + decode: SchemaGetter.transform((value) => mapJsonKeys(value, toCamelKey)), + encode: SchemaGetter.transform( + (value) => mapJsonKeys(value, toSnakeKey) as Record, + ), + }), + ) + // The widget schemas below (transform, data source, display, layout, widget) are // a deliberate re-declaration of the stored schema in // `packages/widgets/src/dashboard/shared/`, differing only by `encodeKeys` for @@ -123,11 +154,62 @@ const V2WidgetTransform = Schema.Struct({ }), ) -export const V2WidgetDataSource = Schema.Struct({ +/** + * The data source, as a discriminated union on `kind`. + * + * Schema v3 replaced the stored `{ endpoint, params }` bag with this union, and + * `/v2/dashboards` republishes the new shape rather than encoding back to the old + * one — a deliberate breaking change, taken so there is exactly one data-source + * shape in the system instead of a wire format kept alive by a translation layer. + * + * Re-declared here rather than aliased to `WidgetDataSourceSchema`, for the same + * reason as every other schema in this file: the v2 wire is snake_case + * throughout, and the stored schema is not. Aliasing compiles and silently ships + * `fieldMap` where the published spec says `field_map`. + * + * `queries`, `formulas` and `comparison` go through `snakeCasedWire`, so their + * wire representation is byte-identical to v2 — where they lived inside the + * untyped `params` bag and got the same recursive snake_case treatment — while + * decoding to the real stored types. Only the envelope around them moved. + */ +const V2QueryDataSource = Schema.Struct({ + kind: Schema.Literal("query"), + resultShape: Schema.Literals(QUERY_RESULT_SHAPES), + queries: Schema.Array(snakeCasedWire(QueryBuilderQueryDraftSchema)), + formulas: optional(Schema.Array(snakeCasedWire(QueryBuilderFormulaSchema))), + comparison: optional(snakeCasedWire(QueryComparisonSchema)), + defaultLimit: optional(Schema.Number), + limit: optional(Schema.Number), + columns: optional(Schema.Array(Schema.String)), + transform: optional(V2WidgetTransform), +}).pipe(Schema.encodeKeys({ resultShape: "result_shape", defaultLimit: "default_limit" })) + +const V2RawSqlDataSource = Schema.Struct({ + kind: Schema.Literal("raw_sql"), + sql: Schema.String, + displayType: optional(Schema.String), + granularitySeconds: optional(Schema.Number), + transform: optional(V2WidgetTransform), +}).pipe(Schema.encodeKeys({ displayType: "display_type", granularitySeconds: "granularity_seconds" })) + +const V2RouteDataSource = Schema.Struct({ + kind: Schema.Literal("route"), endpoint: Schema.String, params: optional(UnknownRecord), transform: optional(V2WidgetTransform), -}).annotate({ identifier: "DashboardWidgetDataSource", title: "Dashboard widget data source" }) +}) + +const V2StaticDataSource = Schema.Struct({ + kind: Schema.Literal("static"), + transform: optional(V2WidgetTransform), +}) + +export const V2WidgetDataSource = Schema.Union([ + V2QueryDataSource, + V2RawSqlDataSource, + V2RouteDataSource, + V2StaticDataSource, +]).annotate({ identifier: "DashboardWidgetDataSource", title: "Dashboard widget data source" }) const V2WidgetDisplayColumn = Schema.Struct({ field: Schema.String, diff --git a/packages/domain/src/http/v2/v2-contract.test.ts b/packages/domain/src/http/v2/v2-contract.test.ts index cd9aaef32..17a51fccb 100644 --- a/packages/domain/src/http/v2/v2-contract.test.ts +++ b/packages/domain/src/http/v2/v2-contract.test.ts @@ -249,8 +249,12 @@ describe("V2Dashboard wire format", () => { { id: "widget-1", visualization: "chart", + // A `route` arm: the only kind that still carries an opaque params + // bag in v3, and therefore the only one that exercises the recursive + // snake_case wire convention asserted below. data_source: { - endpoint: "queryBuilderTimeseries", + kind: "route", + endpoint: "service_overview", params: { start_time: "now-1h", nested_filter: { attribute_key: "service.name" } }, transform: { field_map: { value: "requests" } }, }, @@ -294,7 +298,9 @@ describe("V2Dashboard wire format", () => { expect(decoded.timeRange.type).toBe("absolute") expect(decoded.refreshIntervalSeconds).toBeNull() expect(decoded.widgets[0]?.dataSource.transform?.fieldMap).toEqual({ value: "requests" }) - expect(decoded.widgets[0]?.dataSource.params).toEqual({ + const decodedSource = decoded.widgets[0]?.dataSource + if (decodedSource?.kind !== "route") throw new Error("expected a route data source") + expect(decodedSource.params).toEqual({ startTime: "now-1h", nestedFilter: { attributeKey: "service.name" }, }) @@ -303,7 +309,11 @@ describe("V2Dashboard wire format", () => { expect(wire.id).toMatch(/^dash_/) expect(wire.time_range).toHaveProperty("start_time") expect(wire.widgets[0]?.data_source.transform).toHaveProperty("field_map") - expect(wire.widgets[0]?.data_source.params).toHaveProperty("nested_filter.attribute_key") + const wireSource = wire.widgets[0]?.data_source + if (wireSource === undefined || !("params" in wireSource)) { + throw new Error("expected a route data source on the wire") + } + expect(wireSource.params).toHaveProperty("nested_filter.attribute_key") expect(wire.widgets[0]?.layout).toHaveProperty("min_w") // Section membership snake_cases; `tabs` is already single-word throughout. expect(wire.widgets[0]).toHaveProperty("section_id", "section-1") diff --git a/packages/widgets/src/dashboard/access.ts b/packages/widgets/src/dashboard/access.ts index e6ed13a2f..57726ffeb 100644 --- a/packages/widgets/src/dashboard/access.ts +++ b/packages/widgets/src/dashboard/access.ts @@ -1,4 +1,5 @@ import type { QuerySet, QueryResultShape } from "@maple/query-model" +import { QUERY_ENDPOINT_SHAPES, RAW_SQL_ENDPOINT } from "./legacy-endpoints" import type { WidgetDataSourceTransformV2 } from "./shared/transform" /** @@ -20,25 +21,11 @@ import type { WidgetDataSourceTransformV2 } from "./shared/transform" const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value) -/** - * The v2 endpoints that carried a user-authored query rather than a fixed route, - * keyed by the result shape that is the v3 identity of the same thing. - * - * Canonical in this direction because `construct.ts` writes it and the MCP - * inspector reports it; the endpoint → shape lookup below is derived, so the two - * cannot drift. - */ -export const QUERY_SHAPE_ENDPOINTS = { - timeseries: "custom_query_builder_timeseries", - breakdown: "custom_query_builder_breakdown", - list: "custom_query_builder_list", -} as const satisfies Record - -const QUERY_ENDPOINT_SHAPES: Record = Object.fromEntries( - Object.entries(QUERY_SHAPE_ENDPOINTS).map(([shape, endpoint]) => [endpoint, shape]), -) as Record - -export const RAW_SQL_ENDPOINT = "raw_sql_chart" +// The endpoint tables moved to `legacy-endpoints.ts` — they are wire +// vocabularies rather than schema, and they outlive this file, which is deleted +// once v3 is the only stored shape. Re-exported here so the ~30 consumers that +// import them from `access.ts` today keep working until then. +export { QUERY_SHAPE_ENDPOINTS, RAW_SQL_ENDPOINT } from "./legacy-endpoints" /** * The endpoint name, for consumers that still dispatch on it. diff --git a/packages/widgets/src/dashboard/construct.test.ts b/packages/widgets/src/dashboard/construct.test.ts index d66fd71cd..e0eaea231 100644 --- a/packages/widgets/src/dashboard/construct.test.ts +++ b/packages/widgets/src/dashboard/construct.test.ts @@ -37,14 +37,23 @@ describe("makeQueryDataSource", () => { it("omits formulas and comparison rather than writing empty keys", () => { // A widget that never had formulas must not become indistinguishable from // one that lost them on the next read-modify-write. - const params = makeQueryDataSource({ resultShape: "list", queries: [] }).params - expect(params).toEqual({ queries: [] }) + expect(makeQueryDataSource({ resultShape: "list", queries: [] })).toEqual({ + kind: "query", + resultShape: "list", + queries: [], + }) }) - it("still writes the v2 endpoint while the stored version is 2", () => { - expect(dataSourceEndpoint(makeQueryDataSource({ resultShape: "breakdown", queries: [] }))).toBe( - "custom_query_builder_breakdown", - ) + // A query data source has no endpoint in v3 — its identity is `kind` plus + // `resultShape`. `dataSourceEndpoint` returning null here rather than + // synthesising `custom_query_builder_breakdown` is the property that stops the + // string-sniffing creeping back in. + it("writes no endpoint for a query source", () => { + const source = makeQueryDataSource({ resultShape: "breakdown", queries: [] }) + + expect(source.kind).toBe("query") + expect(source.resultShape).toBe("breakdown") + expect(dataSourceEndpoint(source)).toBeNull() }) it("carries a transform through untouched", () => { @@ -88,6 +97,9 @@ describe("makeRouteDataSource", () => { }) it("omits an absent params bag rather than writing an empty one", () => { - expect(makeRouteDataSource("service_overview")).toEqual({ endpoint: "service_overview" }) + expect(makeRouteDataSource("service_overview")).toEqual({ + kind: "route", + endpoint: "service_overview", + }) }) }) diff --git a/packages/widgets/src/dashboard/construct.ts b/packages/widgets/src/dashboard/construct.ts index 042694173..1da82d532 100644 --- a/packages/widgets/src/dashboard/construct.ts +++ b/packages/widgets/src/dashboard/construct.ts @@ -1,7 +1,12 @@ import type { QueryResultShape, QuerySet } from "@maple/query-model" -import { QUERY_SHAPE_ENDPOINTS, RAW_SQL_ENDPOINT, type RawSqlDataSource } from "./access" +import type { RawSqlDataSource } from "./access" import type { WidgetDataSourceTransformV2 } from "./shared/transform" -import type { WidgetDataSourceV2 } from "./v2/data-source" +import type { + QueryWidgetDataSource, + RawSqlWidgetDataSource, + RouteWidgetDataSource, + StaticWidgetDataSource, +} from "./v3/data-source" /** * Writing a widget's data source without caring which schema version stores it. @@ -12,13 +17,16 @@ import type { WidgetDataSourceV2 } from "./v2/data-source" * a *meaning* ("a timeseries query set", "some raw SQL") to a function instead of * hand-assembling an endpoint string and an untyped bag. * - * While `CURRENT_DASHBOARD_SCHEMA_VERSION` is 2 these emit `{ endpoint, params }`. - * At the flip they emit the typed v3 union and no call site changes. The - * round-trip tests in `construct.test.ts` are what hold that promise: every + * These now emit the typed v3 union. That switch happened here and NOWHERE ELSE: + * the ~40 call sites — every dashboard template, the Perses importer, the MCP + * widget builders, `widget-definitions.ts` — are untouched by the flip, because + * they were already handing over a meaning rather than assembling a bag. That was + * the whole point of extracting these, and it is the return on it. + * + * The round-trip tests in `construct.test.ts` are what hold the promise: every * constructor's output must read back through the matching accessor unchanged. */ -type WidgetDataSource = typeof WidgetDataSourceV2.Type type WidgetDataSourceTransform = typeof WidgetDataSourceTransformV2.Type export interface QueryDataSourceInput extends QuerySet { @@ -45,26 +53,27 @@ export interface QueryDataSourceInput extends QuerySet { /** * A widget backed by a user-authored query set. * - * The endpoint stays a literal in the return type rather than widening to - * `string`: the web app narrows `WidgetDataSource["endpoint"]` to its registry's - * key union so `serverFunctionMap` is statically total, and a widened `string` - * would make every call site here unassignable to it. + * `resultShape` stays a type parameter rather than widening to + * `QueryResultShape`, so a caller that passes a literal gets it back in the + * return type. That is what lets the web app's chart picker narrow on the shape + * it just constructed without a cast. */ export const makeQueryDataSource = ( input: QueryDataSourceInput & { readonly resultShape: S }, -): WidgetDataSource & { endpoint: (typeof QUERY_SHAPE_ENDPOINTS)[S] } => ({ - endpoint: QUERY_SHAPE_ENDPOINTS[input.resultShape], - params: { - queries: input.queries, - // Absent rather than empty when the caller has none: `dataSourceQuerySet` - // reads both as "no formulas", and writing the empty key back would make a - // widget that never had formulas indistinguishable from one that lost them. - ...(input.formulas === undefined ? {} : { formulas: input.formulas }), - ...(input.comparison === undefined ? {} : { comparison: input.comparison }), - ...(input.defaultLimit === undefined ? {} : { defaultLimit: input.defaultLimit }), - ...(input.limit === undefined ? {} : { limit: input.limit }), - ...(input.columns === undefined ? {} : { columns: input.columns }), - }, +): typeof QueryWidgetDataSource.Type & { readonly resultShape: S } => ({ + kind: "query", + resultShape: input.resultShape, + queries: input.queries, + // Absent rather than empty when the caller has none: `dataSourceQuerySet` + // reads both as "no formulas", and writing the empty key back would make a + // widget that never had formulas indistinguishable from one that lost them. + // Mandatory under `optionalKey`, where a present `undefined` is a decode error + // — which is most of why these constructors still earn their place in v3. + ...(input.formulas === undefined ? {} : { formulas: input.formulas }), + ...(input.comparison === undefined ? {} : { comparison: input.comparison }), + ...(input.defaultLimit === undefined ? {} : { defaultLimit: input.defaultLimit }), + ...(input.limit === undefined ? {} : { limit: input.limit }), + ...(input.columns === undefined ? {} : { columns: input.columns }), ...(input.transform === undefined ? {} : { transform: input.transform }), }) @@ -73,15 +82,11 @@ export interface RawSqlDataSourceInput extends RawSqlDataSource { } /** A widget backed by user-authored ClickHouse SQL. */ -export const makeRawSqlDataSource = ( - input: RawSqlDataSourceInput, -): WidgetDataSource & { endpoint: typeof RAW_SQL_ENDPOINT } => ({ - endpoint: RAW_SQL_ENDPOINT, - params: { - sql: input.sql, - ...(input.displayType === undefined ? {} : { displayType: input.displayType }), - ...(input.granularitySeconds === undefined ? {} : { granularitySeconds: input.granularitySeconds }), - }, +export const makeRawSqlDataSource = (input: RawSqlDataSourceInput): typeof RawSqlWidgetDataSource.Type => ({ + kind: "raw_sql", + sql: input.sql, + ...(input.displayType === undefined ? {} : { displayType: input.displayType }), + ...(input.granularitySeconds === undefined ? {} : { granularitySeconds: input.granularitySeconds }), ...(input.transform === undefined ? {} : { transform: input.transform }), }) @@ -89,16 +94,35 @@ export const makeRawSqlDataSource = ( * A widget backed by one of the curated fixed routes (`service_overview`, …). * * These keep an endpoint name and an opaque params bag in v3 too — the bag is - * per-route and closing it is a separate, much larger job — so this constructor - * exists for symmetry and to give the sweep one shape to grep for, not because - * the call site would otherwise break at the flip. + * per-route and closing it is a separate, much larger job. + * + * `E` stays a type parameter so the literal survives into the return type. That + * is now the ONLY compile-time check on a route name: `RouteWidgetDataSource` + * types `endpoint` as an open `Schema.String` on purpose, because closing the + * STORED schema would make one stale route name a decode failure that locks a + * whole dashboard out of editing. Open in storage, checked at authoring. */ export const makeRouteDataSource = ( endpoint: E, params?: Record, transform?: WidgetDataSourceTransform, -): WidgetDataSource & { endpoint: E } => ({ +): typeof RouteWidgetDataSource.Type & { readonly endpoint: E } => ({ + kind: "route", endpoint, ...(params === undefined ? {} : { params }), ...(transform === undefined ? {} : { transform }), }) + +/** + * A widget that issues no request at all — today, a markdown note. + * + * In v2 this was `makeRouteDataSource("markdown_static")`, a route pointing at a + * no-op server function that existed only so the registry lookup would succeed. + * The union answers "this widget fetches nothing" by its type instead. + */ +export const makeStaticDataSource = ( + transform?: WidgetDataSourceTransform, +): typeof StaticWidgetDataSource.Type => ({ + kind: "static", + ...(transform === undefined ? {} : { transform }), +}) diff --git a/packages/widgets/src/dashboard/document-helpers.ts b/packages/widgets/src/dashboard/document-helpers.ts index 498293f84..54e2619db 100644 --- a/packages/widgets/src/dashboard/document-helpers.ts +++ b/packages/widgets/src/dashboard/document-helpers.ts @@ -1,5 +1,5 @@ import type { IsoDateTimeString } from "@maple/primitives" -import { DashboardDocumentV2 } from "./v2/document" +import { DashboardDocumentV3 } from "./v3/document" /** * Carry a stored dashboard forward with a new widget array. @@ -19,7 +19,7 @@ import { DashboardDocumentV2 } from "./v2/document" * rejects). Building the props by hand is what made that dance look necessary. */ export const withWidgets = ( - document: DashboardDocumentV2, - widgets: DashboardDocumentV2["widgets"], + document: DashboardDocumentV3, + widgets: DashboardDocumentV3["widgets"], updatedAt: IsoDateTimeString, -): DashboardDocumentV2 => new DashboardDocumentV2({ ...document, widgets, updatedAt }) +): DashboardDocumentV3 => new DashboardDocumentV3({ ...document, widgets, updatedAt }) diff --git a/packages/widgets/src/dashboard/index.ts b/packages/widgets/src/dashboard/index.ts index cffcc05aa..482d0bf0b 100644 --- a/packages/widgets/src/dashboard/index.ts +++ b/packages/widgets/src/dashboard/index.ts @@ -35,9 +35,34 @@ export { makeQueryDataSource, makeRawSqlDataSource, makeRouteDataSource, + makeStaticDataSource, type QueryDataSourceInput, type RawSqlDataSourceInput, } from "./construct" +// v3 — what the unsuffixed aliases below now point at. The suffixed names stay +// exported for the upgrade transform and its tests, which must name a version +// explicitly. +export { + QueryWidgetDataSource, + RawSqlWidgetDataSource, + RouteWidgetDataSource, + StaticWidgetDataSource, + WIDGET_DATA_SOURCE_KINDS, + type WidgetDataSourceKind, + WidgetDataSourceV3, +} from "./v3/data-source" +export { DashboardDocumentV3, PortableDashboardDocumentV3 } from "./v3/document" +export { DashboardWidgetV3, WidgetDisplayConfigV3 } from "./v3/widget" +export { MARKDOWN_STATIC_ENDPOINT, QUERY_ENDPOINT_SHAPES } from "./legacy-endpoints" +// The one-shot upgrade the backfill script runs against Postgres. Deliberately +// not a `DashboardMigration` — see the header of `upgrade-to-v3.ts`. +export { + fromLegacyDataSource, + isDocumentV3, + isV3DataSource, + upgradeDocumentToV3, + upgradeStoredDocument, +} from "./upgrade-to-v3" export { type DashboardParseOutcome, parseStoredDashboard, stampCurrentVersion } from "./parse" export { CURRENT_DASHBOARD_SCHEMA_VERSION, DashboardSchemaVersion } from "./version" export { makeWidgetDisplayConfigSchema } from "./shared/display" @@ -77,14 +102,14 @@ export { // through a subclass of a `Schema.Class` still constructs the *parent*, so // `instanceof` on a decoded value would be false. export { - DashboardDocumentV2 as DashboardDocument, - PortableDashboardDocumentV2 as PortableDashboardDocument, -} from "./v2/document" + DashboardDocumentV3 as DashboardDocument, + PortableDashboardDocumentV3 as PortableDashboardDocument, +} from "./v3/document" export { withWidgets } from "./document-helpers" export { DASHBOARD_GRID_COLS, findNextPosition, type PlaceableWidget } from "./placement" -export { WidgetDataSourceV2 as WidgetDataSourceSchema } from "./v2/data-source" -export { WidgetDisplayConfigV2 as WidgetDisplayConfigSchema } from "./v2/widget" -export { DashboardWidgetV2 as DashboardWidgetSchema } from "./v2/widget" +export { WidgetDataSourceV3 as WidgetDataSourceSchema } from "./v3/data-source" +export { WidgetDisplayConfigV3 as WidgetDisplayConfigSchema } from "./v3/widget" +export { DashboardWidgetV3 as DashboardWidgetSchema } from "./v3/widget" // Section helpers: pure placement/repair logic shared by the API write path, // the web read path and the tests. diff --git a/packages/widgets/src/dashboard/legacy-endpoints.ts b/packages/widgets/src/dashboard/legacy-endpoints.ts new file mode 100644 index 000000000..7a0564788 --- /dev/null +++ b/packages/widgets/src/dashboard/legacy-endpoints.ts @@ -0,0 +1,55 @@ +import type { QueryResultShape } from "@maple/query-model" + +/** + * The legacy endpoint vocabulary — NOT schema. + * + * Before v3 a data source was `{ endpoint, params }`, and the endpoint string + * carried the source's identity: `"custom_query_builder_timeseries"` meant "a + * query set returning a timeseries", `"raw_sql_chart"` meant raw SQL. v3 makes + * that identity structural (`kind` + `resultShape`), so the strings stop being + * schema. + * + * They do not stop existing. Two contracts still speak them, and both outlive + * the stored v2 shape: + * + * 1. The web app's server-function registry (`data-source-registry.ts`) is keyed + * by endpoint string, and that key is also the atom family key, the retention + * namespace, and the `LIST_ENDPOINTS` membership test. + * 2. The public v2 HTTP API (`/v2/dashboards`) emits and accepts + * `{ endpoint, params }` and always will — a version number in a URL exists + * precisely so the internal shape can move without breaking published clients. + * + * They live in their own file, away from the schema modules, so that this + * distinction survives contact with the next refactor: these are wire + * vocabularies that happen to be strings, not a shape anything is stored in. + */ + +/** + * The endpoints that carried a user-authored query set, keyed by the result + * shape that is their v3 identity. + * + * Canonical in this direction because the v2 API encoder writes it and the MCP + * inspector reports it; the endpoint -> shape lookup is derived below, so the + * two cannot drift. + */ +export const QUERY_SHAPE_ENDPOINTS = { + timeseries: "custom_query_builder_timeseries", + breakdown: "custom_query_builder_breakdown", + list: "custom_query_builder_list", +} as const satisfies Record + +export const QUERY_ENDPOINT_SHAPES: Record = Object.fromEntries( + Object.entries(QUERY_SHAPE_ENDPOINTS).map(([shape, endpoint]) => [endpoint, shape]), +) as Record + +export const RAW_SQL_ENDPOINT = "raw_sql_chart" + +/** + * The one route that never had a query: a markdown note. + * + * It was a `route` in v2 with a no-op server function behind it, and becomes + * `{ kind: "static" }` in v3. Named here rather than inlined because three + * separate places have to agree on the mapping (the migration, the v2 API + * encoder, and the web registry that stops needing a server function for it). + */ +export const MARKDOWN_STATIC_ENDPOINT = "markdown_static" diff --git a/packages/widgets/src/dashboard/migrations/index.ts b/packages/widgets/src/dashboard/migrations/index.ts index d28862855..845283bdd 100644 --- a/packages/widgets/src/dashboard/migrations/index.ts +++ b/packages/widgets/src/dashboard/migrations/index.ts @@ -14,7 +14,10 @@ export const DASHBOARD_MIGRATIONS: ReadonlyArray = [v1ToV2] const isPlainObject = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value) -const KNOWN_SCHEMA_VERSIONS: ReadonlySet = new Set([1, 2]) +// Includes 3 even though no chain step produces it: `detectSchemaVersion` reads +// an unrecognised version as 1, so leaving 3 out would make a backfilled document +// look like a pre-versioning one and run it through `v1ToV2` again. +const KNOWN_SCHEMA_VERSIONS: ReadonlySet = new Set([1, 2, 3]) /** Absent, non-numeric, or unrecognised `schemaVersion` all read as version 1. */ export const detectSchemaVersion = (document: unknown): DashboardSchemaVersion => { @@ -60,5 +63,17 @@ export const migrateToLatest = (document: unknown): Record => { version = step.to } - return { ...current, schemaVersion: CURRENT_DASHBOARD_SCHEMA_VERSION } + // Stamped with the version actually REACHED, not with the current one. + // + // These differ during the v3 window: the chain ends at 2 while + // `CURRENT_DASHBOARD_SCHEMA_VERSION` is 3, because the v2 -> v3 transform is a + // one-shot backfill (`upgrade-to-v3.ts`) rather than a chain step. Stamping 3 + // on a document still in v2 shape would be a lie that decode immediately + // catches — `parseStoredDashboard` returns `Rejected` and the writable path + // refuses it, so nothing corrupt reaches storage — but a lie the version + // history and the span annotation would both repeat. + // + // This whole module is deleted once the backfill has run; until then it should + // report what is true. + return { ...current, schemaVersion: version } } diff --git a/packages/widgets/src/dashboard/migrations/migrations.test.ts b/packages/widgets/src/dashboard/migrations/migrations.test.ts index cf79c8178..2427ddaef 100644 --- a/packages/widgets/src/dashboard/migrations/migrations.test.ts +++ b/packages/widgets/src/dashboard/migrations/migrations.test.ts @@ -2,6 +2,7 @@ import { Effect, Schema } from "effect" import { describe, expect, it } from "vitest" import { parseStoredDashboard, stampCurrentVersion } from "../parse" import { CURRENT_DASHBOARD_SCHEMA_VERSION } from "../version" +import { upgradeStoredDocument } from "../upgrade-to-v3" import { DASHBOARD_MIGRATIONS, detectSchemaVersion, migrateToLatest } from "./index" /** A minimal but complete v1 document, as a pre-versioning row would be stored. */ @@ -47,7 +48,17 @@ const looseDocument = { ], } -const parse = (payload: unknown) => Effect.runSync(parseStoredDashboard(payload)) +/** + * Reads a stored payload the way the BACKFILL does — chain, then the one-shot + * v2 -> v3 upgrade — and then decodes. + * + * `parseStoredDashboard` alone no longer suffices for a legacy fixture: the + * decoder is v3 while `migrateToLatest` only reaches v2, because the v2 -> v3 + * step is a one-shot backfill rather than a chain entry. That gap IS the + * migration window, and asserting through `upgradeStoredDocument` is what makes + * these tests cover the path that actually closes it. + */ +const parse = (payload: unknown) => Effect.runSync(parseStoredDashboard(upgradeStoredDocument(payload))) const firstWidget = (document: Record): Record => { const widgets = document.widgets @@ -58,14 +69,19 @@ const firstWidget = (document: Record): Record } describe("the migration chain", () => { - it("is contiguous and terminates at the current version", () => { + // The chain no longer runs all the way to the current version, and that is the + // design rather than a gap: the v2 -> v3 step is a one-shot backfill + // (`upgrade-to-v3.ts`), not a chain entry, so the chain stops at 2 while + // current is 3. What must still hold is contiguity from 1 — a hole in the + // chain silently skips a document. + it("is contiguous from 1", () => { let expected = 1 for (const migration of DASHBOARD_MIGRATIONS) { expect(migration.from).toBe(expected) expect(migration.to).toBe(expected + 1) expected = migration.to } - expect(expected).toBe(CURRENT_DASHBOARD_SCHEMA_VERSION) + expect(expected).toBeLessThanOrEqual(CURRENT_DASHBOARD_SCHEMA_VERSION) }) // Run idempotence over the *loose* document too: a step that coerces is only @@ -156,7 +172,10 @@ describe("the v1 -> v2 step", () => { expect(outcome._tag).toBe("Decoded") if (outcome._tag !== "Decoded") return - expect(outcome.fromVersion).toBe(1) + // The stored version is read off the raw payload: `parse` here upgrades + // before decoding, so the outcome reports the version it decoded, not the + // one the row was written in. + expect(detectSchemaVersion(looseDocument)).toBe(1) expect(outcome.document.widgets[0]?.visualization).toBe("chart") }) @@ -238,30 +257,44 @@ describe("migrateToLatest with a document from a newer build", () => { expect(migrateToLatest(fromTheFuture).schemaVersion).toBe(99) }) - it("still migrates a document at or below the current version", () => { - expect(migrateToLatest(legacyDocument).schemaVersion).toBe(CURRENT_DASHBOARD_SCHEMA_VERSION) + // `migrateToLatest` stamps the version it actually REACHED, which is where the + // chain ends (2) — not the current version (3). Only `upgradeStoredDocument`, + // which applies the one-shot v2 -> v3 on top, can honestly stamp 3. + it("stamps the version the chain actually reached", () => { + const chainEnd = DASHBOARD_MIGRATIONS[DASHBOARD_MIGRATIONS.length - 1]?.to ?? 1 + expect(migrateToLatest(legacyDocument).schemaVersion).toBe(chainEnd) + }) + + it("only claims the current version once the v3 upgrade has run too", () => { + const upgraded = upgradeStoredDocument(legacyDocument) as Record + expect(upgraded.schemaVersion).toBe(CURRENT_DASHBOARD_SCHEMA_VERSION) }) }) describe("parseStoredDashboard", () => { - it("decodes a legacy unstamped document and reports the version it came from", () => { + it("decodes a legacy unstamped document once it has been upgraded", () => { const outcome = parse(legacyDocument) + // The version the row was STORED in is read off the raw payload — `parse` + // here upgrades first, so the outcome reports the version it decoded. + expect(detectSchemaVersion(legacyDocument)).toBe(1) expect(outcome._tag).toBe("Decoded") if (outcome._tag !== "Decoded") return - expect(outcome.fromVersion).toBe(1) + expect(outcome.fromVersion).toBe(CURRENT_DASHBOARD_SCHEMA_VERSION) expect(outcome.degradedWidgetIds).toEqual([]) expect(outcome.document.name).toBe("Legacy board") expect(outcome.document.widgets).toHaveLength(1) }) - it("carries widget params through byte-for-byte", () => { + // v1 -> v2 carried `params` byte-for-byte; the v3 upgrade rewrites the bag into + // typed fields, so the assertion becomes "the queries survived the reshaping". + it("carries the widget's queries through the reshaping", () => { const outcome = parse(legacyDocument) if (outcome._tag !== "Decoded") throw new Error("expected Decoded") - expect(outcome.document.widgets[0]?.dataSource.params).toEqual( - legacyDocument.widgets[0]!.dataSource.params, - ) + const dataSource = outcome.document.widgets[0]?.dataSource + if (dataSource?.kind !== "query") throw new Error("expected a query data source") + expect(dataSource.queries).toEqual(legacyDocument.widgets[0]!.dataSource.params.queries) }) it("rejects a structurally corrupt document instead of half-decoding it", () => { @@ -269,7 +302,6 @@ describe("parseStoredDashboard", () => { expect(outcome._tag).toBe("Rejected") if (outcome._tag !== "Rejected") return - expect(outcome.fromVersion).toBe(1) // The issue names the offending path so a 503 is diagnosable from logs. expect(outcome.issue).not.toBe("") }) diff --git a/packages/widgets/src/dashboard/parse.ts b/packages/widgets/src/dashboard/parse.ts index 98589da80..cd7ae65bc 100644 --- a/packages/widgets/src/dashboard/parse.ts +++ b/packages/widgets/src/dashboard/parse.ts @@ -1,6 +1,6 @@ import { Effect, Schema, SchemaIssue } from "effect" import { detectSchemaVersion, migrateToLatest } from "./migrations" -import { DashboardDocumentV2 } from "./v2/document" +import { DashboardDocumentV3 } from "./v3/document" import { CURRENT_DASHBOARD_SCHEMA_VERSION, type DashboardSchemaVersion } from "./version" /** @@ -16,7 +16,7 @@ import { CURRENT_DASHBOARD_SCHEMA_VERSION, type DashboardSchemaVersion } from ". export type DashboardParseOutcome = | { readonly _tag: "Decoded" - readonly document: DashboardDocumentV2 + readonly document: DashboardDocumentV3 /** The version the payload was stored in, before migration. */ readonly fromVersion: DashboardSchemaVersion /** @@ -33,7 +33,7 @@ export type DashboardParseOutcome = readonly issue: string } -const decodeDocument = Schema.decodeUnknownEffect(DashboardDocumentV2) +const decodeDocument = Schema.decodeUnknownEffect(DashboardDocumentV3) /** Path-anchored, newline-joined rendering of the issue tree. */ const formatIssue = SchemaIssue.makeFormatterDefault() diff --git a/packages/widgets/src/dashboard/upgrade-to-v3.test.ts b/packages/widgets/src/dashboard/upgrade-to-v3.test.ts new file mode 100644 index 000000000..994139ea6 --- /dev/null +++ b/packages/widgets/src/dashboard/upgrade-to-v3.test.ts @@ -0,0 +1,214 @@ +import { Schema } from "effect" +import { describe, expect, it } from "vitest" +import { + dataSourceEndpoint, + dataSourceQuerySet, + dataSourceRawSql, + dataSourceRouteParams, + dataSourceTransform, +} from "./access" +import { MARKDOWN_STATIC_ENDPOINT, QUERY_SHAPE_ENDPOINTS, RAW_SQL_ENDPOINT } from "./legacy-endpoints" +import { DashboardDocumentV3 } from "./v3/document" +import { isDocumentV3, upgradeDocumentToV3 } from "./upgrade-to-v3" + +const query = { id: "a", name: "A", dataSource: "traces", aggregation: "count" } + +/** One v2 data source per legacy endpoint family — the full input space. */ +const V2_SOURCES: ReadonlyArray<{ label: string; source: Record }> = [ + ...Object.entries(QUERY_SHAPE_ENDPOINTS).map(([shape, endpoint]) => ({ + label: `query/${shape}`, + source: { + endpoint, + params: { + queries: [query], + formulas: [{ id: "f", name: "F", expression: "a * 2", legend: "F" }], + defaultLimit: 20, + limit: 5, + columns: ["a", "b"], + }, + transform: { reduceToValue: { field: "value", aggregate: "first" } }, + }, + })), + { + label: "raw_sql", + source: { + endpoint: RAW_SQL_ENDPOINT, + params: { sql: "SELECT 1 WHERE $__orgFilter", displayType: "line", granularitySeconds: 60 }, + }, + }, + { + label: "route", + source: { endpoint: "service_overview", params: { serviceName: "api" } }, + }, + { + label: "route without params", + source: { endpoint: "list_traces" }, + }, + { + label: "static", + source: { endpoint: MARKDOWN_STATIC_ENDPOINT }, + }, +] + +const documentWith = (dataSource: unknown, display: unknown = { title: "T" }) => ({ + id: "3f1b7c62-5a1e-4d0f-9a3b-6c2e8d4f1a90", + schemaVersion: 2, + name: "Board", + timeRange: { type: "relative", value: "1h" }, + widgets: [ + { + id: "widget-1", + visualization: "chart", + dataSource, + display, + layout: { x: 0, y: 0, w: 6, h: 4 }, + }, + ], + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", +}) + +/** + * `upgradeDocumentToV3` returns `unknown` on purpose — it takes raw stored JSON + * and makes no promise about a document it did not understand. These two helpers + * do the narrowing once, so no assertion below has to. + */ +const upgraded = (document: unknown): Record => { + const result = upgradeDocumentToV3(document) + if (typeof result !== "object" || result === null) throw new Error("expected an object") + return result as Record +} + +const firstWidget = (document: unknown): Record => { + const widgets = upgraded(document).widgets + if (!Array.isArray(widgets)) throw new Error("expected widgets to be an array") + const [widget] = widgets + if (typeof widget !== "object" || widget === null) throw new Error("expected a widget object") + return widget as Record +} + +const migratedSource = (dataSource: unknown): unknown => firstWidget(documentWith(dataSource)).dataSource + +describe("the v3 upgrade preserves meaning", () => { + // The strongest assertion available, and it is nearly free: `access.ts` reads + // BOTH shapes, so asserting that every accessor returns the same answer before + // and after literally says "the transform changed the envelope, not the + // content". If this passes for every input family, the ~30 consumers already + // on the accessors cannot observe the flip. + it.each(V2_SOURCES)("$label reads identically through every accessor", ({ source }) => { + const v3 = migratedSource(source) + + expect(dataSourceQuerySet(v3)).toEqual(dataSourceQuerySet(source)) + expect(dataSourceRawSql(v3)).toEqual(dataSourceRawSql(source)) + expect(dataSourceRouteParams(v3)).toEqual(dataSourceRouteParams(source)) + expect(dataSourceTransform(v3)).toEqual(dataSourceTransform(source)) + }) + + // The one deliberate exception, asserted so it cannot regress silently: a v3 + // query/raw_sql source has NO endpoint, and `dataSourceEndpoint` returns null + // rather than inventing one. Inventing it would re-create the string-sniffing + // the union removes. + it.each(V2_SOURCES)("$label reports an endpoint only when it is a route", ({ source }) => { + const v3 = migratedSource(source) + const kind = (v3 as { kind: string }).kind + + expect(dataSourceEndpoint(v3)).toBe(kind === "route" ? dataSourceEndpoint(source) : null) + }) +}) + +describe("the v3 upgrade totality and idempotence", () => { + it.each(V2_SOURCES)("$label is idempotent", ({ source }) => { + const once = migratedSource(source) + expect(migratedSource(once)).toEqual(once) + }) + + it.each([ + { label: "widgets is not an array", document: { widgets: "nope" } }, + { label: "widget is not an object", document: { widgets: [42] } }, + { label: "widget has no dataSource", document: { widgets: [{ id: "w" }] } }, + ])("returns $label unchanged rather than throwing", ({ document }) => { + expect(() => upgradeDocumentToV3(document)).not.toThrow() + }) + + it("carries an unrecognised endpoint through as a route, losing nothing", () => { + const source = { endpoint: "some_endpoint_this_build_never_heard_of", params: { a: 1 } } + expect(migratedSource(source)).toEqual({ + kind: "route", + endpoint: "some_endpoint_this_build_never_heard_of", + params: { a: 1 }, + }) + }) + + it("defaults a missing raw SQL string to empty rather than dropping the widget", () => { + expect(migratedSource({ endpoint: RAW_SQL_ENDPOINT, params: {} })).toEqual({ + kind: "raw_sql", + sql: "", + }) + }) +}) + +describe("the v3 upgrade recurses into display.sparkline.dataSource", () => { + // `display.sparkline.dataSource` embeds a full data source, and `v1ToV2` never + // had to recurse — so there is no precedent in this directory that would have + // caught a missed sparkline. + it("migrates the sparkline's own data source", () => { + const document = documentWith( + { endpoint: MARKDOWN_STATIC_ENDPOINT }, + { + title: "T", + sparkline: { + enabled: true, + dataSource: { endpoint: QUERY_SHAPE_ENDPOINTS.timeseries, params: { queries: [query] } }, + }, + }, + ) + const display = firstWidget(document).display as { sparkline: { dataSource: unknown } } + + expect(display.sparkline.dataSource).toEqual({ + kind: "query", + resultShape: "timeseries", + queries: [query], + }) + }) + + it("leaves a sparkline with no data source alone", () => { + const document = documentWith({ endpoint: "list_traces" }, { sparkline: { enabled: false } }) + expect(firstWidget(document).display).toEqual({ sparkline: { enabled: false } }) + }) +}) + +describe("the v3 upgrade output decodes as v3", () => { + const decode = Schema.decodeUnknownSync(DashboardDocumentV3) + + it.each(V2_SOURCES)("$label produces a decodable document", ({ source }) => { + expect(() => decode({ ...upgraded(documentWith(source)), schemaVersion: 3 })).not.toThrow() + }) +}) + +// The backfill's "is this row done?" test. It has to agree with the transform, +// or the script either skips rows it should convert or rewrites rows forever. +describe("isDocumentV3", () => { + it.each(V2_SOURCES)("is false for a stored $label, true once upgraded", ({ source }) => { + expect(isDocumentV3(documentWith(source))).toBe(false) + expect(isDocumentV3(upgradeDocumentToV3(documentWith(source)))).toBe(true) + }) + + it("is false when only the sparkline is left behind", () => { + const halfDone = documentWith( + { kind: "static" }, + { sparkline: { enabled: true, dataSource: { endpoint: "list_traces" } } }, + ) + expect(isDocumentV3(halfDone)).toBe(false) + expect(isDocumentV3(upgradeDocumentToV3(halfDone))).toBe(true) + }) + + // A document with nothing to convert counts as done, so the backfill leaves it + // alone rather than rewriting every empty dashboard on every run. + it.each([ + { label: "no widgets array", document: {} }, + { label: "an empty widget list", document: { widgets: [] } }, + { label: "a widget with no data source", document: { widgets: [{ id: "w" }] } }, + ])("treats $label as done", ({ document }) => { + expect(isDocumentV3(document)).toBe(true) + }) +}) diff --git a/packages/widgets/src/dashboard/upgrade-to-v3.ts b/packages/widgets/src/dashboard/upgrade-to-v3.ts new file mode 100644 index 000000000..0d249e8d6 --- /dev/null +++ b/packages/widgets/src/dashboard/upgrade-to-v3.ts @@ -0,0 +1,208 @@ +import type { QueryResultShape, QuerySet } from "@maple/query-model" +import { MARKDOWN_STATIC_ENDPOINT, QUERY_ENDPOINT_SHAPES, RAW_SQL_ENDPOINT } from "./legacy-endpoints" +import { migrateToLatest } from "./migrations" +import { CURRENT_DASHBOARD_SCHEMA_VERSION } from "./version" +import type { WidgetDataSourceTransformV2 } from "./shared/transform" +import type { WidgetDataSourceV3 } from "./v3/data-source" + +/** + * The one-shot upgrade of a stored dashboard document from the v2 + * `{ endpoint, params }` data-source bag to the v3 discriminated union. + * + * Deliberately NOT a `DashboardMigration` and not part of `DASHBOARD_MIGRATIONS`. + * The chain exists to migrate documents lazily, in memory, on every read, for as + * long as a row goes unwritten — the right design when two shapes must coexist + * indefinitely. They do not here: this runs ONCE against Postgres via the + * backfill script, and after it every stored row is v3. Registering it as a chain + * step would mean carrying a permanent read-path branch to serve rows that no + * longer exist. + * + * There is no inverse. An encoder back to `{ endpoint, params }` existed while the + * public `/v2` API was going to keep the legacy wire shape; that API now emits v3 + * too, so nothing needs the downgrade and it is gone rather than kept "just in + * case". + * + * Total and idempotent, because a backfill must be safe to re-run: a document it + * does not understand comes back unchanged rather than throwing, and a document + * already in v3 is returned as-is. Both properties are what let the script be + * killed halfway and started again. + */ + +type V3DataSource = typeof WidgetDataSourceV3.Type +type Transform = typeof WidgetDataSourceTransformV2.Type + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value) + +/** Spread-if-present, so an absent `optionalKey` never becomes a present `undefined`. */ +const put = (key: K, value: V | undefined): Record | { [P in K]: V } => + value === undefined ? ({} as Record) : ({ [key]: value } as { [P in K]: V }) + +/** + * TOTAL and never throws — this runs inside the backfill, which must be safe to + * re-run and must never fail on a stored document however malformed. Everything + * is read defensively and anything unrecognised falls through to a `route`, the + * arm that preserves an arbitrary endpoint plus an opaque bag verbatim. The worst case is therefore "stored unchanged in + * a different envelope", never "data dropped". + */ +export const fromLegacyDataSource = (dataSource: unknown): V3DataSource => { + if (!isRecord(dataSource)) return { kind: "static" } + + const transform = isRecord(dataSource.transform) + ? put("transform", dataSource.transform as Transform) + : {} + const endpoint = typeof dataSource.endpoint === "string" ? dataSource.endpoint : null + const params = isRecord(dataSource.params) ? dataSource.params : {} + + if (endpoint === null) { + // Already v3? Return it as-is so the migration is idempotent. This is the + // same structural test the backfill uses to decide a row is done. + if (typeof dataSource.kind === "string") return dataSource as V3DataSource + return { kind: "static", ...transform } + } + + if (endpoint === MARKDOWN_STATIC_ENDPOINT) return { kind: "static", ...transform } + + if (endpoint === RAW_SQL_ENDPOINT) { + return { + kind: "raw_sql", + // The v2 accessor coerced a missing `sql` to "" and callers relied on it; + // v3 makes the field required, so the coercion happens here instead — + // once, at the boundary, rather than on every read. + sql: typeof params.sql === "string" ? params.sql : "", + ...put("displayType", typeof params.displayType === "string" ? params.displayType : undefined), + ...put( + "granularitySeconds", + typeof params.granularitySeconds === "number" ? params.granularitySeconds : undefined, + ), + ...transform, + } + } + + const resultShape: QueryResultShape | undefined = QUERY_ENDPOINT_SHAPES[endpoint] + if (resultShape !== undefined) { + return { + kind: "query", + resultShape, + queries: Array.isArray(params.queries) ? (params.queries as QuerySet["queries"]) : [], + ...put( + "formulas", + Array.isArray(params.formulas) ? (params.formulas as QuerySet["formulas"]) : undefined, + ), + ...put( + "comparison", + isRecord(params.comparison) ? (params.comparison as QuerySet["comparison"]) : undefined, + ), + ...put("defaultLimit", typeof params.defaultLimit === "number" ? params.defaultLimit : undefined), + ...put("limit", typeof params.limit === "number" ? params.limit : undefined), + ...put( + "columns", + Array.isArray(params.columns) ? (params.columns as ReadonlyArray) : undefined, + ), + ...transform, + } + } + + return { + kind: "route", + endpoint, + ...put("params", isRecord(dataSource.params) ? dataSource.params : undefined), + ...transform, + } +} + +/** + * Already v3 iff it carries a string `kind`. + * + * Structural rather than a flag or a version column, so re-running is inherently + * a no-op and no cursor state can cause a double transform. The backfill uses the + * same test to decide a row is done. + */ +export const isV3DataSource = (dataSource: unknown): boolean => + isRecord(dataSource) && typeof dataSource.kind === "string" + +const upgradeDataSource = (dataSource: unknown): unknown => + isV3DataSource(dataSource) ? dataSource : fromLegacyDataSource(dataSource) + +/** + * `display.sparkline.dataSource` embeds a FULL data source, so it needs the same + * treatment as the widget's own. + * + * Easy to miss: `v1ToV2` never had to recurse, so there is no precedent in this + * package to copy. A sparkline left in v2 shape decodes as a `route` whose + * endpoint is `custom_query_builder_timeseries`, which renders an empty sparkline + * rather than failing loudly — a silent bug that would survive any test checking + * only the widget's top-level data source. + */ +const upgradeDisplay = (display: unknown): unknown => { + if (!isRecord(display)) return display + const sparkline = display.sparkline + if (!isRecord(sparkline) || sparkline.dataSource === undefined) return display + return { + ...display, + sparkline: { ...sparkline, dataSource: upgradeDataSource(sparkline.dataSource) }, + } +} + +const upgradeWidget = (widget: unknown): unknown => { + if (!isRecord(widget)) return widget + + const next: Record = { ...widget } + if (widget.dataSource !== undefined) next.dataSource = upgradeDataSource(widget.dataSource) + if (widget.display !== undefined) next.display = upgradeDisplay(widget.display) + return next +} + +export const upgradeDocumentToV3 = (document: unknown): unknown => { + if (!isRecord(document)) return document + if (!Array.isArray(document.widgets)) return document + return { ...document, widgets: document.widgets.map(upgradeWidget) } +} + +/** + * The complete stored-payload upgrade: whatever version a row is in, to v3. + * + * THE function the backfill script calls, and the reason it exists rather than + * the script calling `upgradeDocumentToV3` directly: a v1 row needs more than a + * data-source rewrite. v1 stored `visualization`, `reduceToValue.aggregate` and + * `sortBy.direction` as open strings, and only `v1ToV2` coerces them into the + * closed sets v2 and v3 both require. Skipping it would convert the data source + * correctly and still leave the document undecodable, quarantining every + * pre-versioning dashboard for a reason that has nothing to do with v3. + * + * `migrateToLatest` walks the chain as far as it reaches (2, since the v2 -> v3 + * step is deliberately not a chain entry), then this applies the one-shot on top. + * Once the backfill has run and `migrations/` is deleted, this collapses to + * `upgradeDocumentToV3` — and by then nothing calls it. + */ +export const upgradeStoredDocument = (payload: unknown): unknown => { + const upgraded = upgradeDocumentToV3(migrateToLatest(payload)) + // Restamp here, not in `migrateToLatest`. That function stamps the version it + // actually REACHED — which is 2, since the chain stops there — and it is right + // to, because on its own it has not produced a v3 document. Only after the + // one-shot has run is the stamp true, so only here can it be written. + return isRecord(upgraded) ? { ...upgraded, schemaVersion: CURRENT_DASHBOARD_SCHEMA_VERSION } : upgraded +} + +/** + * True when every data source in the document — widget-level and sparkline — is + * already v3. + * + * The backfill's "is this row done?" predicate and the verification query's + * in-process equivalent. Kept beside the transform so the two cannot disagree + * about what "done" means. + */ +export const isDocumentV3 = (document: unknown): boolean => { + if (!isRecord(document) || !Array.isArray(document.widgets)) return true + + return document.widgets.every((widget) => { + if (!isRecord(widget)) return true + if (widget.dataSource !== undefined && !isV3DataSource(widget.dataSource)) return false + + const display = widget.display + if (!isRecord(display)) return true + const sparkline = display.sparkline + if (!isRecord(sparkline) || sparkline.dataSource === undefined) return true + return isV3DataSource(sparkline.dataSource) + }) +} diff --git a/packages/widgets/src/dashboard/v3/data-source.ts b/packages/widgets/src/dashboard/v3/data-source.ts new file mode 100644 index 000000000..f02e1b1a4 --- /dev/null +++ b/packages/widgets/src/dashboard/v3/data-source.ts @@ -0,0 +1,147 @@ +import { QUERY_RESULT_SHAPES, QuerySetSchema } from "@maple/query-model" +import { Schema } from "effect" +import { UnknownRecord, WidgetDataSourceTransformV2 } from "../shared/transform" + +/** + * v3 data source: a discriminated union instead of an opaque params bag. + * + * v1 and v2 both stored `{ endpoint: string, params?: unknown }`. The endpoint + * string carried the source's identity and `params` carried its content, which + * meant every reader had to know which endpoint names implied which param keys + * — a contract enforced nowhere. v3 makes both structural: `kind` says what the + * source *is*, and each arm declares the fields that arm actually has. + * + * The four arms are the four things a widget can be backed by, and they were + * always these four — v2 just spelled them as string conventions. + */ + +/** + * `transform` is on every arm, not hoisted above the union. + * + * It describes what to do with the *response*, so it is genuinely independent of + * how the request is described — but hoisting it would mean + * `Schema.Struct({ transform, ...union })`, and a struct-of-union loses the + * discriminant narrowing that is this schema's entire point. Repeating one + * optional field on four arms is the cheaper trade, and `ds.transform` still + * reads directly off the union because TypeScript distributes the access. + */ +const transformField = { transform: Schema.optionalKey(WidgetDataSourceTransformV2) } + +/** + * A user-authored query set, from the query builder. + * + * The query-set fields are *spread*, not nested under a `querySet` key. Two + * reasons, both load-bearing: + * + * 1. The shape the ~30 existing consumers already speak is flat — + * `WidgetQuerySet extends QuerySet` in `access.ts` — and + * `packages/query-engine/src/query-set/dispatch.ts` consumes that flat shape + * *structurally* (it deliberately does not import `@maple/widgets`). Nesting + * would break that structural match silently, at runtime. + * 2. A field added to `QuerySetSchema` — shared with alert rules — reaches + * widgets without an edit here. + * + * `resultShape` is required, where `dataSourceQuerySet` used to default it to + * `"timeseries"` for a v2 source missing it. That default now lives in exactly + * one place, the v2 -> v3 migration, which is where a default for legacy data + * belongs. + */ +export const QueryWidgetDataSource = Schema.Struct({ + kind: Schema.Literal("query"), + resultShape: Schema.Literals(QUERY_RESULT_SHAPES), + ...QuerySetSchema.fields, + /** + * Request shaping — how many rows to fetch and which columns. + * + * Deliberately NOT in `QuerySetSchema`: these describe the *request a widget + * makes*, not the query it stores, and an alert rule sharing the query set has + * no use for any of them. `defaultLimit` is the breakdown's + * fetch-past-what-you-draw allowance (only the pie collapses a long tail into + * "Other"); `limit`/`columns` are the list shape's row cap and projection. + */ + defaultLimit: Schema.optionalKey(Schema.Number), + limit: Schema.optionalKey(Schema.Number), + columns: Schema.optionalKey(Schema.Array(Schema.String)), + ...transformField, +}).annotate({ + identifier: "@maple/QueryWidgetDataSource", + title: "Query widget data source", +}) + +/** User-authored ClickHouse SQL. */ +export const RawSqlWidgetDataSource = Schema.Struct({ + kind: Schema.Literal("raw_sql"), + // Required, unlike the v2 accessor which coerced a missing `sql` to "". An + // empty string is still representable and still meaningful (a raw-SQL widget + // saved before any SQL was written); an *absent* one is not. + sql: Schema.String, + displayType: Schema.optionalKey(Schema.String), + granularitySeconds: Schema.optionalKey(Schema.Number), + ...transformField, +}).annotate({ + identifier: "@maple/RawSqlWidgetDataSource", + title: "Raw SQL widget data source", +}) + +/** + * One of the curated fixed routes (`service_overview`, `list_traces`, ...). + * + * The only arm that keeps an opaque params bag. Closing it is per-route across + * ~20 routes and is a separate, much larger job — the point of v3 is that the + * two arms carrying *user-authored* content are typed, which is where the + * string-sniffing actually hurt. + * + * `endpoint` is `Schema.String`, deliberately open. Closing it to a literal set + * would make a document naming a route this build doesn't know FAIL TO DECODE, + * and `DashboardPersistenceService.parsePayload` turns a decode failure into a + * hard error on the *writable* path — so one stale route name would lock a whole + * dashboard out of editing. Storage stays open; the compile-time typo check + * lives on `makeRouteDataSource`'s type parameter instead, which catches it at + * authoring time without putting stored documents at risk. + */ +export const RouteWidgetDataSource = Schema.Struct({ + kind: Schema.Literal("route"), + endpoint: Schema.String, + params: Schema.optionalKey(UnknownRecord), + ...transformField, +}).annotate({ + identifier: "@maple/RouteWidgetDataSource", + title: "Route widget data source", +}) + +/** + * A widget that reads nothing — today, a markdown note. + * + * In v2 this was a route (`markdown_static`) with a no-op server function behind + * it purely so the registry lookup would succeed. Making it an arm means the + * "this widget issues no request" case is answered by the type rather than by a + * string comparison against one magic endpoint name. + */ +export const StaticWidgetDataSource = Schema.Struct({ + kind: Schema.Literal("static"), + ...transformField, +}).annotate({ + identifier: "@maple/StaticWidgetDataSource", + title: "Static widget data source", +}) + +/** + * The discriminant values, exported so tests can assert coverage. + * + * `dashboard-widget-parity.test.ts` drives its public-API encoder coverage off + * this: a fifth arm added to the union fails that test until someone decides + * what the v2 HTTP API does with it. That is the structural successor to the + * assignability assertions the union replaced. + */ +export const WIDGET_DATA_SOURCE_KINDS = ["query", "raw_sql", "route", "static"] as const +export type WidgetDataSourceKind = (typeof WIDGET_DATA_SOURCE_KINDS)[number] + +export const WidgetDataSourceV3 = Schema.Union([ + QueryWidgetDataSource, + RawSqlWidgetDataSource, + RouteWidgetDataSource, + StaticWidgetDataSource, +]).annotate({ + identifier: "@maple/WidgetDataSource", + title: "Widget data source", +}) diff --git a/packages/widgets/src/dashboard/v3/document.ts b/packages/widgets/src/dashboard/v3/document.ts new file mode 100644 index 000000000..5e7d734a3 --- /dev/null +++ b/packages/widgets/src/dashboard/v3/document.ts @@ -0,0 +1,18 @@ +import { Schema } from "effect" +import { makeDashboardDocumentFields } from "../shared/document" +import { DashboardWidgetV3 } from "./widget" + +const fields = makeDashboardDocumentFields({ widget: DashboardWidgetV3 }) + +/** + * Declared as its own class rather than subclassing the v2 one: decoding through + * a subclass of a `Schema.Class` still constructs the *parent*, so `instanceof` + * on a decoded value would be false. See the note in `../index.ts`. + */ +export class PortableDashboardDocumentV3 extends Schema.Class( + "PortableDashboardDocument", +)(fields.portable) {} + +export class DashboardDocumentV3 extends Schema.Class("DashboardDocument")( + fields.document, +) {} diff --git a/packages/widgets/src/dashboard/v3/widget.ts b/packages/widgets/src/dashboard/v3/widget.ts new file mode 100644 index 000000000..1289979b2 --- /dev/null +++ b/packages/widgets/src/dashboard/v3/widget.ts @@ -0,0 +1,20 @@ +import { Schema } from "effect" +import { WIDGET_VISUALIZATIONS } from "../../widget-types" +import { makeDashboardWidgetSchemas } from "../shared/widget" +import { WidgetDataSourceV3 } from "./data-source" + +/** + * v3 changes only the data source; `visualization` keeps the closed set v2 + * introduced. + * + * Because `makeDashboardWidgetSchemas` derives the display config from the data + * source, `display.sparkline.dataSource` becomes the v3 union here too — which + * is exactly why the migration and both API encoders have to recurse into it. + */ +const v3 = makeDashboardWidgetSchemas({ + visualization: Schema.Literals(WIDGET_VISUALIZATIONS), + dataSource: WidgetDataSourceV3, +}) + +export const WidgetDisplayConfigV3 = v3.display +export const DashboardWidgetV3 = v3.widget diff --git a/packages/widgets/src/dashboard/version.ts b/packages/widgets/src/dashboard/version.ts index c56306005..a94b639ae 100644 --- a/packages/widgets/src/dashboard/version.ts +++ b/packages/widgets/src/dashboard/version.ts @@ -11,10 +11,10 @@ import { Schema } from "effect" * was introduced after those documents were written, so absence is the only * signal they can carry. `detectSchemaVersion` encodes that. */ -export const DashboardSchemaVersion = Schema.Literals([1, 2]).annotate({ +export const DashboardSchemaVersion = Schema.Literals([1, 2, 3]).annotate({ identifier: "@maple/DashboardSchemaVersion", title: "Dashboard Schema Version", }) export type DashboardSchemaVersion = typeof DashboardSchemaVersion.Type -export const CURRENT_DASHBOARD_SCHEMA_VERSION = 2 satisfies DashboardSchemaVersion +export const CURRENT_DASHBOARD_SCHEMA_VERSION = 3 satisfies DashboardSchemaVersion From 386ded71598b2136a402b800842cfc6d35e0ae07 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Fri, 14 Aug 2026 01:15:58 +0200 Subject: [PATCH 13/13] feat(db): add the one-shot dashboard data-source v3 backfill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrites `dashboards.payload_json` and `dashboard_versions.snapshot_json` from the v2 `{ endpoint, params }` bag to the v3 union. Runs once per branch; the deployed code decodes v3 only, so the window between that deploy and this run is one where un-backfilled dashboards fail to load. Rehearsed against local Postgres: 53 dashboards + 116 snapshots converted, zero quarantined, re-run is a clean no-op, and `--restore-from` puts all 53 back. Also pins `effect` in `packages/db`. It had no direct dependency and resolved a transitive `4.0.0-beta.85` while `@maple/widgets` used `rc.108` — two Effect instances, so a schema built by one could not be decoded by the other. Every row classified as undecodable in the first dry run. Nothing was written (dry run is the default), but a script that reported 53/55 dashboards as broken would have been believed if it had been the other way around. --- bun.lock | 2039 +---------------- packages/db/package.json | 7 +- .../backfill-dashboard-datasource-v3.ts | 355 +++ 3 files changed, 392 insertions(+), 2009 deletions(-) create mode 100644 packages/db/scripts/backfill-dashboard-datasource-v3.ts diff --git a/bun.lock b/bun.lock index 30ea06b23..cae3711bb 100644 --- a/bun.lock +++ b/bun.lock @@ -188,55 +188,6 @@ "vitest": "catalog:", }, }, - "apps/mobile": { - "name": "mobile", - "version": "1.0.0", - "dependencies": { - "@clerk/expo": "^3.1.8", - "@expo-google-fonts/geist-mono": "^0.4.1", - "@expo/metro-runtime": "^55.0.9", - "@expo/ui": "~55.0.10", - "@legendapp/list": "^2.0.19", - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/api-logs": "^0.205.0", - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/exporter-logs-otlp-http": "^0.205.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.205.0", - "@opentelemetry/resources": "^2.0.0", - "@opentelemetry/sdk-logs": "^0.205.0", - "@opentelemetry/sdk-trace-base": "^2.0.0", - "@opentelemetry/semantic-conventions": "^1.36.0", - "@react-native-async-storage/async-storage": "2.2.0", - "@tanstack/react-query": "^5.95.2", - "expo": "~55.0.12", - "expo-auth-session": "~55.0.12", - "expo-constants": "~55.0.12", - "expo-crypto": "~55.0.13", - "expo-dev-client": "~55.0.23", - "expo-font": "^55.0.6", - "expo-haptics": "~55.0.14", - "expo-linking": "~55.0.11", - "expo-router": "~55.0.11", - "expo-secure-store": "~55.0.12", - "expo-splash-screen": "^55.0.16", - "expo-ui-ext": "file:./modules/expo-ui-ext", - "expo-web-browser": "~55.0.13", - "react": "19.2.0", - "react-dom": "19.2.0", - "react-native": "0.83.4", - "react-native-gesture-handler": "~2.30.0", - "react-native-safe-area-context": "~5.6.2", - "react-native-screens": "~4.23.0", - "react-native-svg": "15.15.3", - "tailwindcss": "^4.2.2", - "uniwind": "^1.6.2", - }, - "devDependencies": { - "@types/react": "~19.2.14", - "babel-preset-expo": "^55.0.16", - "typescript": "catalog:tooling", - }, - }, "apps/scraper": { "name": "@maple/scraper", "dependencies": { @@ -577,11 +528,13 @@ "@electric-sql/pglite": "^0.5.2", "@maple/domain": "workspace:*", "drizzle-orm": "^0.45.1", + "effect": "catalog:effect", "postgres": "^3.4.9", }, "devDependencies": { "@cloudflare/workers-types": "4.20260603.1", "@effect/language-service": "catalog:effect", + "@maple/widgets": "workspace:*", "@types/node": "catalog:tooling", "drizzle-kit": "^0.31.9", "typescript": "catalog:tooling", @@ -801,8 +754,6 @@ }, }, "packages": { - "@adraffy/ens-normalize": ["@adraffy/ens-normalize@1.11.1", "", {}, "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.161", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.41", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-aV5j1JFKzwfq4RCuWRDerzOsNkIZ7+Pbz4aaGESW7BLbuQMGlsOJTv24+Uyksi1cg5WTEKNik6ghLfFRsU7TKw=="], "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.63", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.41" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EmrD7iRboidulu6yHfMiMhd6RQSw8KrIWhNLK8vl5brQZbIjXkyhUU+FULZM3P4m46Vatzx8u3vX1w/qmFUmqA=="], @@ -891,176 +842,36 @@ "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], - "@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="], - - "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], + "@babel/generator": ["@babel/generator@8.0.0", "", { "dependencies": { "@babel/parser": "^8.0.0", "@babel/types": "^8.0.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "@types/jsesc": "^2.5.0", "jsesc": "^3.0.2" } }, "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g=="], "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], - "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="], - - "@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg=="], - "@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.8", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "debug": "^4.4.3", "lodash.debounce": "^4.0.8", "resolve": "^1.22.11" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA=="], "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="], - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], - "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="], - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - "@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-wrap-function": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og=="], - - "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="], - - "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="], - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], - "@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw=="], - "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], - "@babel/highlight": ["@babel/highlight@7.25.9", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw=="], - "@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], - "@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-syntax-decorators": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg=="], - - "@babel/plugin-proposal-export-default-from": ["@babel/plugin-proposal-export-default-from@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-p+G5BNXDcy3bOXplhY4HybQ1GxH3i2Tppmdm/3epyRu2VgJJZuUlZ61MqRTg582Q7ZLBdP7fePYvsumSEkMxcQ=="], - - "@babel/plugin-syntax-async-generators": ["@babel/plugin-syntax-async-generators@7.8.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw=="], - - "@babel/plugin-syntax-bigint": ["@babel/plugin-syntax-bigint@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg=="], - - "@babel/plugin-syntax-class-properties": ["@babel/plugin-syntax-class-properties@7.12.13", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA=="], - - "@babel/plugin-syntax-class-static-block": ["@babel/plugin-syntax-class-static-block@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw=="], - - "@babel/plugin-syntax-decorators": ["@babel/plugin-syntax-decorators@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg=="], - - "@babel/plugin-syntax-dynamic-import": ["@babel/plugin-syntax-dynamic-import@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ=="], - - "@babel/plugin-syntax-export-default-from": ["@babel/plugin-syntax-export-default-from@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-foag0BB37ROhdeIX9O8G0jX7hw0UekJc04cHMrYLOnrErsnBKqJGHJ8eDRpoCFZBvEPPygmmtw4qyU97qa4oOw=="], - - "@babel/plugin-syntax-flow": ["@babel/plugin-syntax-flow@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ=="], - - "@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg=="], - - "@babel/plugin-syntax-import-meta": ["@babel/plugin-syntax-import-meta@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g=="], - - "@babel/plugin-syntax-json-strings": ["@babel/plugin-syntax-json-strings@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA=="], - - "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="], - - "@babel/plugin-syntax-logical-assignment-operators": ["@babel/plugin-syntax-logical-assignment-operators@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig=="], - - "@babel/plugin-syntax-nullish-coalescing-operator": ["@babel/plugin-syntax-nullish-coalescing-operator@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ=="], - - "@babel/plugin-syntax-numeric-separator": ["@babel/plugin-syntax-numeric-separator@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug=="], - - "@babel/plugin-syntax-object-rest-spread": ["@babel/plugin-syntax-object-rest-spread@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA=="], - - "@babel/plugin-syntax-optional-catch-binding": ["@babel/plugin-syntax-optional-catch-binding@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q=="], - - "@babel/plugin-syntax-optional-chaining": ["@babel/plugin-syntax-optional-chaining@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg=="], - - "@babel/plugin-syntax-private-property-in-object": ["@babel/plugin-syntax-private-property-in-object@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg=="], - - "@babel/plugin-syntax-top-level-await": ["@babel/plugin-syntax-top-level-await@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw=="], - - "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA=="], - - "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ=="], - - "@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-remap-async-to-generator": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA=="], - - "@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-remap-async-to-generator": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w=="], - - "@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ=="], - - "@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA=="], - - "@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A=="], - - "@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g=="], - - "@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/template": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA=="], - - "@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg=="], - - "@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA=="], - - "@babel/plugin-transform-flow-strip-types": ["@babel/plugin-transform-flow-strip-types@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-syntax-flow": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ=="], - - "@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ=="], - - "@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.29.7", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg=="], - - "@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw=="], - - "@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q=="], - - "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ=="], - - "@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ=="], - - "@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg=="], - - "@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw=="], - - "@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.29.7", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-transform-destructuring": "^7.29.7", "@babel/plugin-transform-parameters": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A=="], - - "@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng=="], - - "@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ=="], - - "@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g=="], - - "@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug=="], - - "@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA=="], - - "@babel/plugin-transform-react-display-name": ["@babel/plugin-transform-react-display-name@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+1wdDMGNb4UPeY3Q4L5yLiYe6TXPXubs4NjrgRFw13hPRLJfEMw2Q5OXkee6/IfdqePIeW4Jjwe3aBh7SdKz4Q=="], - - "@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-module-imports": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-syntax-jsx": "^7.29.7", "@babel/types": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A=="], - - "@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.29.7", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Xfy3UVMF04+ypnFbkhvfqtmvwfe92qwQdbGZVonhE+6v35GzlofmOnA1szaZqzb9xYWr0nl1e5EMmzi0DNON1g=="], - "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="], "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="], - "@babel/plugin-transform-react-pure-annotations": ["@babel/plugin-transform-react-pure-annotations@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-H5E+HBgDpr6Q5t+Aj11tL7XkIui1jhbIoArVQnqjgXo5/3YxkN7ZEBcWF4RQlB0T4rrxJQbXS6kiFV6B7XTqUA=="], - - "@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.29.8", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg=="], - "@babel/plugin-transform-runtime": ["@babel/plugin-transform-runtime@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xmAscdE/AsqRW7vutbPNoUmu/nF5SrLKPs7aoJgEjo35lLKA/Bc0i2rMv/hr1+Y0o1bQCiVtith3u2vdgRL39Q=="], - "@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg=="], - - "@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.29.8", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-4S9ksMGVWUshvgK0mKfvZky7leuG5/uoFVwMpAomJ8bMoDJiNHRVmc1EglwW/CmGVSqqWpEbXm9FmbRit22qoA=="], - - "@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA=="], - - "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw=="], - - "@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA=="], - - "@babel/preset-react": ["@babel/preset-react@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "@babel/plugin-transform-react-display-name": "^7.29.7", "@babel/plugin-transform-react-jsx": "^7.29.7", "@babel/plugin-transform-react-jsx-development": "^7.29.7", "@babel/plugin-transform-react-pure-annotations": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-C+PV1TFUPTmBQGoPBL8j2QmLpZ117YTCwxIZeJOM96GbYMFSc7/pOXU5lVykwnZxyTqQxRsvoRk6f2FktZgGHA=="], - - "@babel/preset-typescript": ["@babel/preset-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "@babel/plugin-syntax-jsx": "^7.29.7", "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ=="], - "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], @@ -1069,8 +880,6 @@ "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], - "@base-org/account": ["@base-org/account@2.0.1", "", { "dependencies": { "@noble/hashes": "1.4.0", "clsx": "1.2.1", "eventemitter3": "5.0.1", "idb-keyval": "6.2.1", "ox": "0.6.9", "preact": "10.24.2", "viem": "^2.31.7", "zustand": "5.0.3" } }, "sha512-tySVNx+vd6XEynZL0uvB10uKiwnAfThr8AbKTwILVG86mPbLAhEOInQIk+uDnvpTvfdUhC1Bi5T/46JvFoLZQQ=="], - "@base-ui/react": ["@base-ui/react@1.6.0", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@base-ui/utils": "0.3.1", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@date-fns/tz": "^1.2.0", "@types/react": "^17 || ^18 || ^19", "date-fns": "^4.0.0", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@date-fns/tz", "@types/react", "date-fns"] }, "sha512-/jzjTWJYXhRFO45Bev9lc3cHbmjzCMpUqbMZ2AgKy/z25mY9B6shGSNcXcjQar9n5doM0KYW1W8fcFv2jZBuMw=="], "@base-ui/utils": ["@base-ui/utils@0.3.1", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.2.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-gFFiltORVmW/N6IILTGxizP3PBpVpysqML1ALY5Vk0mH+7faVkCknOU31goYHN5Aoek2dkjxva1XOD2Ce9WuIg=="], @@ -1095,14 +904,8 @@ "@clerk/backend": ["@clerk/backend@2.33.6", "", { "dependencies": { "@clerk/shared": "^3.47.8", "@clerk/types": "^4.101.26", "standardwebhooks": "^1.0.0", "tslib": "2.8.1" } }, "sha512-5foMmTHEQFt4mDv7AN0RDwUHZhGcg/JYe5hnl9SU/LFS8ZHY29Z2HDtIBmzKgjRfOJTMKj1AM81LgK2UFsGm9Q=="], - "@clerk/clerk-js": ["@clerk/clerk-js@6.25.13", "", { "dependencies": { "@base-org/account": "2.0.1", "@clerk/shared": "^4.25.10", "@coinbase/wallet-sdk": "4.3.7", "@solana/wallet-adapter-base": "0.9.27", "@solana/wallet-adapter-react": "0.15.39", "@solana/wallet-standard": "1.1.4", "@stripe/stripe-js": "5.6.0", "@swc/helpers": "0.5.21", "@tanstack/query-core": "^5.100.6", "@wallet-standard/core": "1.1.1", "@zxcvbn-ts/core": "3.0.4", "@zxcvbn-ts/language-common": "3.0.4", "alien-signals": "2.0.6", "browser-tabs-lock": "1.3.0", "core-js": "3.47.0", "crypto-js": "^4.2.0", "dequal": "2.0.3" } }, "sha512-QXhNzo0BjVSEItBsj9ukreofsi9XSrR5OJpB03ZDMb0QwWB5oXDHBBukC7DyHNmWt+dNHq01k4XUmA6cCvnNjA=="], - "@clerk/clerk-react": ["@clerk/clerk-react@5.61.3", "", { "dependencies": { "@clerk/shared": "^3.47.2", "tslib": "2.8.1" }, "peerDependencies": { "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" } }, "sha512-W21aNEeHtqh3xJLuW5g2ydben/1D5pSnxsl/kCnv0IY1zma7lO+aIJ7Br2bR4FKKkiu695mPnjtY+fvkQmCXBg=="], - "@clerk/expo": ["@clerk/expo@3.7.8", "", { "dependencies": { "@clerk/clerk-js": "^6.25.5", "@clerk/react": "^6.12.5", "@clerk/shared": "^4.25.5", "base-64": "^1.0.0", "react-native-url-polyfill": "2.0.0", "tslib": "2.8.1" }, "peerDependencies": { "@clerk/expo-passkeys": ">=0.0.6", "expo": ">=53 <58", "expo-apple-authentication": ">=7.0.0", "expo-auth-session": ">=5", "expo-constants": ">=12", "expo-crypto": ">=12", "expo-local-authentication": ">=13.5.0", "expo-secure-store": ">=12.4.0", "expo-web-browser": ">=12.5.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "react-native": ">=0.75" }, "optionalPeers": ["@clerk/expo-passkeys", "expo-apple-authentication", "expo-auth-session", "expo-constants", "expo-crypto", "expo-local-authentication", "expo-secure-store", "expo-web-browser", "react-dom"] }, "sha512-kfMlD/myFLcmbKUOHcekx7EFSuoUFbLmLgqQKMZFE6dwRXu2etsKoAV622c4j0Nbgubf+qwJId8eoS4vikRNGw=="], - - "@clerk/react": ["@clerk/react@6.12.10", "", { "dependencies": { "@clerk/shared": "^4.25.10", "tslib": "2.8.1" }, "peerDependencies": { "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" } }, "sha512-VDYyCyzRdMsNW6qQsMp1+L2VOXxFwpLfWBxOWPvUNOYCruecnAZjUYEJWS+E+i5MAPhSew9HjuweS4bO0FclTQ=="], - "@clerk/shared": ["@clerk/shared@3.47.8", "", { "dependencies": { "csstype": "3.1.3", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.7", "std-env": "^3.9.0", "swr": "2.3.4" }, "peerDependencies": { "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-cPURU1it/Eal4uXO9SOcHzw9sfE6Opi21W8EJUg+Ri80Q6JKtK8qBmyOpIyqgf09EanexhTzS4xrmYlvn2p7Iw=="], "@clerk/themes": ["@clerk/themes@2.4.57", "", { "dependencies": { "@clerk/shared": "^3.47.2", "tslib": "2.8.1" } }, "sha512-Nb3bO79rMTU/MPVTC/dde6LG27/IgOMKIYi5KSvAmO4ZUHlj0OWufu6CMvz5OYVZ0YdyMnTBU2aPGRUiRzO+2w=="], @@ -1127,8 +930,6 @@ "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260603.1", "", {}, "sha512-TLeVHoBbcYv35S5TdRWUoj3IJ56BhHtrsuci+O7ithU8yz7ttNdCk6rAl1QUSGNVEWSIp54bWOuV/xmX1zu79g=="], - "@coinbase/wallet-sdk": ["@coinbase/wallet-sdk@4.3.7", "", { "dependencies": { "@noble/hashes": "^1.4.0", "clsx": "^1.2.1", "eventemitter3": "^5.0.1", "preact": "^10.24.2", "viem": "^2.27.2" } }, "sha512-z6e5XDw6EF06RqkeyEa+qD0dZ2ZbLci99vx3zwDY//XO8X7166tqKJrR2XlQnzVmtcUuJtCd5fCvr9Cu6zzX7w=="], - "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], "@csstools/color-helpers": ["@csstools/color-helpers@6.1.0", "", {}, "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg=="], @@ -1197,8 +998,6 @@ "@effect/vitest": ["@effect/vitest@4.0.0-rc.108", "", { "peerDependencies": { "effect": "^4.0.0-rc.108", "vitest": ">=4.1.0 <5.0.0" } }, "sha512-XD2GP1JATN28wnIeFGsBqYMuQDCHIodKKgFulGyG91GvuHqgf2gz+WxR2LPdyNoKMtr7lsbRzVj07niSYtIKzA=="], - "@egjs/hammerjs": ["@egjs/hammerjs@2.0.17", "", { "dependencies": { "@types/hammerjs": "^2.0.36" } }, "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A=="], - "@electric-sql/client": ["@electric-sql/client@1.5.22", "", { "dependencies": { "@microsoft/fetch-event-source": "^2.0.1" }, "optionalDependencies": { "@rollup/rollup-darwin-arm64": "^4.18.1" }, "bin": { "intent": "bin/intent.mjs" } }, "sha512-bOlxcMHc39OUnvXNKa/rxtBTPwpxk/SerasFFi8Psw3825/uGvsT7br8YTWzMaETxWu6kvaPc5CXdYTyx5eJgw=="], "@electric-sql/pglite": ["@electric-sql/pglite@0.5.4", "", {}, "sha512-yYZUyyXrHU7tPlCjwZQJ6hIG9DscdCCn7Uk0mYKwC1FeHX286AbcmFveMiRBEak8e9iPupjsoVImN3yJZVed2g=="], @@ -1271,72 +1070,6 @@ "@exodus/bytes": ["@exodus/bytes@1.15.1", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q=="], - "@expo-google-fonts/geist-mono": ["@expo-google-fonts/geist-mono@0.4.3", "", {}, "sha512-gjnwv1QOG4meZpMhrLjTIPC/+lhyNu8EcXa2eJPVbPrYvguVjCfBq+Ezq6cWKWW8ung6fqb2TjvuTalE6lqlnw=="], - - "@expo-google-fonts/material-symbols": ["@expo-google-fonts/material-symbols@0.4.42", "", {}, "sha512-KZmHZRcthJ3KFZZlpzHjopA9guZgWR9fb3uVZlTR0BNlvG2pw1bnYBCpkze2PB0vRllwGhAM7lWXsfmcWCbXYg=="], - - "@expo/cli": ["@expo/cli@55.0.34", "", { "dependencies": { "@expo/code-signing-certificates": "^0.0.6", "@expo/config": "~55.0.19", "@expo/config-plugins": "~55.0.11", "@expo/devcert": "^1.2.1", "@expo/env": "~2.1.3", "@expo/image-utils": "^0.8.15", "@expo/json-file": "^10.0.15", "@expo/log-box": "55.0.13", "@expo/metro": "~55.1.1", "@expo/metro-config": "~55.0.25", "@expo/osascript": "^2.4.4", "@expo/package-manager": "^1.10.6", "@expo/plist": "^0.5.4", "@expo/prebuild-config": "^55.0.20", "@expo/require-utils": "^55.0.6", "@expo/router-server": "^55.0.18", "@expo/schema-utils": "^55.0.5", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.4.0", "@react-native/dev-middleware": "0.83.10", "accepts": "^1.3.8", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.1.0", "bplist-parser": "^0.3.1", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "dnssd-advertise": "^1.1.6", "expo-server": "^55.0.11", "fetch-nodeshim": "^0.4.10", "getenv": "^2.0.0", "glob": "^13.0.0", "lan-network": "^0.2.1", "multitars": "^1.0.0", "node-forge": "^1.3.3", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^4.0.3", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "resolve-from": "^5.0.0", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "terminal-link": "^2.1.1", "toqr": "^0.1.1", "wrap-ansi": "^7.0.0", "ws": "^8.12.1", "zod": "^3.25.76" }, "peerDependencies": { "expo": "*", "expo-router": "*", "react-native": "*" }, "optionalPeers": ["expo-router", "react-native"], "bin": { "expo-internal": "build/bin/cli" } }, "sha512-b+PnIWiIUxXmSi09hmbzeBNlygP/W1uuRQU6cm6ke9P5sBEknbZ4cpBY0Xz4L1L3Pr6u/BUbIW0KXJBOppzAww=="], - - "@expo/code-signing-certificates": ["@expo/code-signing-certificates@0.0.6", "", { "dependencies": { "node-forge": "^1.3.3" } }, "sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w=="], - - "@expo/config": ["@expo/config@55.0.19", "", { "dependencies": { "@expo/config-plugins": "~55.0.11", "@expo/config-types": "^55.0.6", "@expo/json-file": "^10.0.15", "@expo/require-utils": "^55.0.6", "deepmerge": "^4.3.1", "getenv": "^2.0.0", "glob": "^13.0.0", "resolve-workspace-root": "^2.0.0", "semver": "^7.6.0", "slugify": "^1.3.4" } }, "sha512-MahrCR6LsElK/1r/C/zfEZ5Pdgmo88Gtd4CCnASv/rC2pUIB+ENt2oKRHAPIWi7McTeoqDPb9KwCkQBpNMOjrg=="], - - "@expo/config-plugins": ["@expo/config-plugins@55.0.11", "", { "dependencies": { "@expo/config-types": "^55.0.6", "@expo/json-file": "~10.0.15", "@expo/plist": "^0.5.4", "@expo/sdk-runtime-versions": "^1.0.0", "chalk": "^4.1.2", "debug": "^4.3.5", "getenv": "^2.0.0", "glob": "^13.0.0", "resolve-from": "^5.0.0", "semver": "^7.5.4", "slugify": "^1.6.6", "xcode": "^3.0.1", "xml2js": "0.6.0" } }, "sha512-85ZSmIK8rMfvYbG/2IHtwnykVdb6LI0ZavduM3ZwUMThyyVegYjVsO5Zek2ec8xzPhCmYX0ar5onnFEcwUDUlw=="], - - "@expo/config-types": ["@expo/config-types@55.0.6", "", {}, "sha512-S+GJKYoIjnWlert/9vXuTohaTsMbyOLSVxdIgPgoq3P4N1p4CWrfyZLnz6qRug8wYSO5fcYcS9mFleyEP8wRLg=="], - - "@expo/devcert": ["@expo/devcert@1.2.1", "", { "dependencies": { "@expo/sudo-prompt": "^9.3.1", "debug": "^3.1.0" } }, "sha512-qC4eaxmKMTmJC2ahwyui6ud8f3W60Ss7pMkpBq40Hu3zyiAaugPXnZ24145U7K36qO9UHdZUVxsCvIpz2RYYCA=="], - - "@expo/devtools": ["@expo/devtools@55.0.3", "", { "dependencies": { "chalk": "^4.1.2" }, "peerDependencies": { "react": "*", "react-native": "*" }, "optionalPeers": ["react", "react-native"] }, "sha512-KoIDgo0NoXeWLsIcOdZqtAG/1LlsM+JL0DA3bo0vCYaOYTBLXi/ZvRBqa20Ub8D2vKLNa+FgRQW0gRg04Ps1Pg=="], - - "@expo/dom-webview": ["@expo/dom-webview@55.0.6", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-ZNm8tiNEZysxrr36J0x4mOCGyJDcaIvL/3tMxBz0VJIJDcV19xjuJAhJQxHovu+jKx6s9tRyEAINa1mdrzV39g=="], - - "@expo/env": ["@expo/env@2.1.3", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "getenv": "^2.0.0" } }, "sha512-Rhu/lJ1kOhqzJvLwW0iB4WKUzB1nFa8WBwXFL44qkSTNwibjrbI8lA46Ix6W2MMTdlUqL1m85Gdyx0T7nGpREg=="], - - "@expo/fingerprint": ["@expo/fingerprint@0.16.8", "", { "dependencies": { "@expo/env": "^2.1.3", "@expo/spawn-async": "^1.7.2", "arg": "^5.0.2", "chalk": "^4.1.2", "debug": "^4.3.4", "getenv": "^2.0.0", "glob": "^13.0.0", "ignore": "^5.3.1", "minimatch": "^10.2.2", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "bin": { "fingerprint": "bin/cli.js" } }, "sha512-RaLOikl+alkvG9ZrBQFGi3kVXJdG5GQXY1H3V1ZFCwFyhFikwuBozVrl4KL7U+N0Tm8Bf1qDmpTEIx8JOyrQog=="], - - "@expo/image-utils": ["@expo/image-utils@0.8.15", "", { "dependencies": { "@expo/require-utils": "^55.0.6", "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "getenv": "^2.0.0", "jimp-compact": "0.16.1", "parse-png": "^2.1.0", "semver": "^7.6.0" } }, "sha512-3f5CgKJnJ8m4mp8VTcAFQqLrxof99RDr77Aa+7hgzDPg3ZotjLnofl77hf+COQRYi/kuqRYdYFgPIqOIOIdWJA=="], - - "@expo/json-file": ["@expo/json-file@10.2.0", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "json5": "^2.2.3" } }, "sha512-S6XzKe3R9GQeHiUPXc3xJjOv2VJhOEwFYf7xdC2z2cUqt3kZJ9mSO877sNQloVdnW/SUCtPY3bexlM7nwq+CAQ=="], - - "@expo/local-build-cache-provider": ["@expo/local-build-cache-provider@55.0.15", "", { "dependencies": { "@expo/config": "~55.0.19", "chalk": "^4.1.2" } }, "sha512-coFNPt2gGmWtTJwqHlvx/Us9/VWK+pTHuP4jv/NM5jaHZuN0FhX7m2FvwthbluHUGpaVBc8+AxEDmDVqTfKtHg=="], - - "@expo/log-box": ["@expo/log-box@55.0.13", "", { "dependencies": { "@expo/dom-webview": "^55.0.6", "anser": "^1.4.9", "stacktrace-parser": "^0.1.10" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-pV623uwyKjw/L1HVWOpwWOu/ISLH1+c+ESVv30alQMbEaE3cLcwcQ+UnHiAGayMBNMQwK57eckOgH40RBXHfCA=="], - - "@expo/metro": ["@expo/metro@55.1.1", "", { "dependencies": { "metro": "0.83.7", "metro-babel-transformer": "0.83.7", "metro-cache": "0.83.7", "metro-cache-key": "0.83.7", "metro-config": "0.83.7", "metro-core": "0.83.7", "metro-file-map": "0.83.7", "metro-minify-terser": "0.83.7", "metro-resolver": "0.83.7", "metro-runtime": "0.83.7", "metro-source-map": "0.83.7", "metro-symbolicate": "0.83.7", "metro-transform-plugins": "0.83.7", "metro-transform-worker": "0.83.7" } }, "sha512-/wfXo5hTuAVpVLG/4hzlmD9NBGJkzkmBEMm/4VICajYRbj7y8OmqqPWbbymzHiBiHB6tI9BnsyXpQM6zVZEECg=="], - - "@expo/metro-config": ["@expo/metro-config@55.0.25", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.20.0", "@babel/generator": "^7.20.5", "@expo/config": "~55.0.19", "@expo/env": "~2.1.3", "@expo/json-file": "~10.0.15", "@expo/metro": "~55.1.1", "@expo/spawn-async": "^1.7.2", "browserslist": "^4.25.0", "chalk": "^4.1.0", "debug": "^4.3.2", "getenv": "^2.0.0", "glob": "^13.0.0", "hermes-parser": "^0.32.0", "jsc-safe-url": "^0.2.4", "lightningcss": "^1.30.1", "picomatch": "^4.0.3", "postcss": "^8.5.14", "resolve-from": "^5.0.0" }, "peerDependencies": { "expo": "*" }, "optionalPeers": ["expo"] }, "sha512-3KXVaIdawin16YXXDK4P3HGYjnlNqq/34dqb0kvoTM9RBDWbuq+zYkanLRHKLrDkDLODv7vSvbQNAATt76my0Q=="], - - "@expo/metro-runtime": ["@expo/metro-runtime@55.0.12", "", { "dependencies": { "@expo/log-box": "55.0.13", "anser": "^1.4.9", "pretty-format": "^29.7.0", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-dom": "*", "react-native": "*" }, "optionalPeers": ["react-dom"] }, "sha512-EeqXrRBvChdt6+brlUkZM5749QoS7OlN7Zsn/AT8hhGV+xNKglirVRkcKQFmKqPgjgmNxfwgLJ6ddanwZ9dapg=="], - - "@expo/osascript": ["@expo/osascript@2.7.1", "", { "dependencies": { "@expo/spawn-async": "^1.8.0" } }, "sha512-Zn03EX6In7ts2lPUW2ESUSkEhEWQN1qqsiXjadtZMJOuZRkMiAg1ZQHuvz9DjByDWNJ2pBwAGyrts9lj9k389g=="], - - "@expo/package-manager": ["@expo/package-manager@1.13.1", "", { "dependencies": { "@expo/json-file": "^11.0.1", "@expo/spawn-async": "^1.8.0", "chalk": "^4.0.0", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "resolve-workspace-root": "^2.0.0" } }, "sha512-y/K+CaYYpZpNGZhSX4HyLT/vyIunFjNfyoxNysPBCefeLKI/VCx6f9LNPzrxayr3rCYO5bl9O8H+HRQK265Nkg=="], - - "@expo/plist": ["@expo/plist@0.5.4", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-Jqppj0FULNq6Zp5JtQrFICl8TtpMjwwUbxEcEC2T3z7m+TOrTQEHZXz3D3Ay7vhbmvD+VMgfWJ4ARclJXeN8Eg=="], - - "@expo/prebuild-config": ["@expo/prebuild-config@55.0.20", "", { "dependencies": { "@expo/config": "~55.0.19", "@expo/config-plugins": "~55.0.11", "@expo/config-types": "^55.0.6", "@expo/image-utils": "^0.8.15", "@expo/json-file": "^10.0.15", "@react-native/normalize-colors": "0.83.10", "debug": "^4.3.1", "resolve-from": "^5.0.0", "semver": "^7.6.0", "xml2js": "0.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-Cy9xnwK0f3aG0TbKkBFuVQCtd6rj22muYfj7o1iCAE0NOUy5ks1k2vcmlKD1g48wzcWw0hFySiwtaGsB9qeVRQ=="], - - "@expo/require-utils": ["@expo/require-utils@55.0.6", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8" }, "peerDependencies": { "typescript": "^5.0.0 || ^5.0.0-0" }, "optionalPeers": ["typescript"] }, "sha512-IqsRGPdGbF0lAIrg3XQ+GzcITYcsskvIRmFAjw2kBnr8RgSrRhFAJY1bKksukEUsuZy0knhTCVIIUce7hocqeA=="], - - "@expo/router-server": ["@expo/router-server@55.0.18", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "@expo/metro-runtime": "^55.0.11", "expo": "*", "expo-constants": "^55.0.16", "expo-font": "^55.0.8", "expo-router": "*", "expo-server": "^55.0.11", "react": "*", "react-dom": "*", "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1" }, "optionalPeers": ["@expo/metro-runtime", "expo-router", "react-dom", "react-server-dom-webpack"] }, "sha512-W0VsvIiR48OvdlAOUlag4qspGYT/DV4srfYowlbYxwZh5Qw0MjiZAID4Zt7F0qynGZZxx8OZPpFhIX7XsqtRmg=="], - - "@expo/schema-utils": ["@expo/schema-utils@55.0.5", "", {}, "sha512-wdV4SzWJ/l+6y/r9vDaqPqXGyfxUD1igbX8rRbRBfkVXenAUY2qenq6HF4S0iJ0pjpVDNTANn5aQY7VV55hhsA=="], - - "@expo/sdk-runtime-versions": ["@expo/sdk-runtime-versions@1.0.0", "", {}, "sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ=="], - - "@expo/spawn-async": ["@expo/spawn-async@1.8.0", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-eb9xxd/LbuEGSdua4NumCu/McVB9EM+F/JxB9pWgnERw4HQ9XyTNH1KapG6oqLWR8TuRK2LQfzJlmNi94CVobw=="], - - "@expo/sudo-prompt": ["@expo/sudo-prompt@9.3.2", "", {}, "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw=="], - - "@expo/ui": ["@expo/ui@55.0.17", "", { "dependencies": { "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-NW/wFs+mhQeA/5W0yUNn6qKRNJBGiuF7NjCLZDFvg5Tyd+hDUEDEhBsLvI9MJ8B2aYuGcbuH9XZBIPF22LMrfQ=="], - - "@expo/vector-icons": ["@expo/vector-icons@15.1.1", "", { "peerDependencies": { "expo-font": ">=14.0.4", "react": "*", "react-native": "*" } }, "sha512-Iu2VkcoI5vygbtYngm7jb4ifxElNVXQYdDrYkT7UCEIiKLeWnQY0wf2ZhHZ+Wro6Sc5TaumpKUOqDRpLi5rkvw=="], - - "@expo/ws-tunnel": ["@expo/ws-tunnel@1.0.6", "", {}, "sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q=="], - - "@expo/xcpretty": ["@expo/xcpretty@4.4.4", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "chalk": "^4.1.0", "js-yaml": "^4.1.0" }, "bin": { "excpretty": "build/cli.js" } }, "sha512-4aQzz9vgxcNXFfo/iyNgDDYfsU5XGKKxWxZopw0cVotHiW+U8IJbIxMaxsINs6bHhtkG3StKNPcOrn3eBuxKPw=="], - "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], "@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="], @@ -1487,24 +1220,6 @@ "@ioredis/commands": ["@ioredis/commands@1.10.0", "", {}, "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q=="], - "@isaacs/ttlcache": ["@isaacs/ttlcache@1.4.1", "", {}, "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA=="], - - "@istanbuljs/load-nyc-config": ["@istanbuljs/load-nyc-config@1.1.0", "", { "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", "get-package-type": "^0.1.0", "js-yaml": "^3.13.1", "resolve-from": "^5.0.0" } }, "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ=="], - - "@istanbuljs/schema": ["@istanbuljs/schema@0.1.6", "", {}, "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw=="], - - "@jest/create-cache-key-function": ["@jest/create-cache-key-function@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3" } }, "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA=="], - - "@jest/environment": ["@jest/environment@29.7.0", "", { "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0" } }, "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw=="], - - "@jest/fake-timers": ["@jest/fake-timers@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ=="], - - "@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], - - "@jest/transform": ["@jest/transform@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", "write-file-atomic": "^4.0.2" } }, "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw=="], - - "@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -1517,8 +1232,6 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@legendapp/list": ["@legendapp/list@2.0.19", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-zDWg8yg0smKxxk+M7gwAbZAnf5uczohPA+IjqLSkImz7+e9ytxeT0Mq35RBO9RTKODOXfV/aIgm1uqUHLBEdmg=="], - "@libsql/client": ["@libsql/client@0.17.4", "", { "dependencies": { "@libsql/core": "^0.17.4", "@libsql/hrana-client": "^0.10.0", "js-base64": "^3.7.5", "libsql": "^0.5.28", "promise-limit": "^2.7.0" } }, "sha512-lYayFWasDV78A+TjlEhr6ubb3odBV6OHjb+wdp8VQcyWWAEIjuwbCHaraEUS4m4yWoo0BvZo96It4VdzZRmRWw=="], "@libsql/core": ["@libsql/core@0.17.4", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-LqF9gIvnJ38nmAH1y/ChizHqDO/MO1wLgA96XrraulEEbqXxLjleSH92YWTolbuJKgPUmGu4aJk9W3UnAcxLOQ=="], @@ -1675,10 +1388,6 @@ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.3", "", { "os": "win32", "cpu": "x64" }, "sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw=="], - "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], - - "@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], - "@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], "@nodable/entities": ["@nodable/entities@3.0.0", "", {}, "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw=="], @@ -1749,9 +1458,7 @@ "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.205.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-wBlPk1nFB37Hsm+3Qy73yQSobVn28F4isnWIBvKpd5IUH/eat8bwcL02H9yzmHyyPmukeccSl2mbN5sDQZYnPg=="], - "@opentelemetry/core": ["@opentelemetry/core@2.10.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ=="], - - "@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.205.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.205.0", "@opentelemetry/core": "2.1.0", "@opentelemetry/otlp-exporter-base": "0.205.0", "@opentelemetry/otlp-transformer": "0.205.0", "@opentelemetry/sdk-logs": "0.205.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-5JteMyVWiro4ghF0tHQjfE6OJcF7UBUcoEqX3UIQ5jutKP1H+fxFdyhqjjpmeHMFxzOHaYuLlNR1Bn7FOjGyJg=="], + "@opentelemetry/core": ["@opentelemetry/core@2.1.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ=="], "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.205.0", "", { "dependencies": { "@opentelemetry/core": "2.1.0", "@opentelemetry/otlp-exporter-base": "0.205.0", "@opentelemetry/otlp-transformer": "0.205.0", "@opentelemetry/resources": "2.1.0", "@opentelemetry/sdk-trace-base": "2.1.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-vr2bwwPCSc9u7rbKc74jR+DXFvyMFQo9o5zs+H/fgbK672Whw/1izUKVf+xfWOdJOvuwTnfWxy+VAY+4TSo74Q=="], @@ -1993,86 +1700,6 @@ "@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="], - "@radix-ui/primitive": ["@radix-ui/primitive@1.1.7", "", {}, "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q=="], - - "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.15", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA=="], - - "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.5", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA=="], - - "@radix-ui/react-context": ["@radix-ui/react-context@1.2.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA=="], - - "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.23", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-focus-guards": "1.1.6", "@radix-ui/react-focus-scope": "1.1.16", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-slot": "1.3.3", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA=="], - - "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg=="], - - "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-effect-event": "0.0.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w=="], - - "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.6", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ=="], - - "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.16", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ=="], - - "@radix-ui/react-id": ["@radix-ui/react-id@1.1.4", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA=="], - - "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.17", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ=="], - - "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.10", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw=="], - - "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.10", "", { "dependencies": { "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg=="], - - "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-is-hydrated": "0.1.3", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ=="], - - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q=="], - - "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.21", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-roving-focus": "1.1.19", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog=="], - - "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ=="], - - "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-use-effect-event": "0.0.5", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ=="], - - "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.5", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg=="], - - "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw=="], - - "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw=="], - - "@react-native-async-storage/async-storage": ["@react-native-async-storage/async-storage@2.2.0", "", { "dependencies": { "merge-options": "^3.0.4" }, "peerDependencies": { "react-native": "^0.0.0-0 || >=0.65 <1.0" } }, "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw=="], - - "@react-native/assets-registry": ["@react-native/assets-registry@0.83.4", "", {}, "sha512-aqKtpbJDSQeSX/Dwv0yMe1/Rd2QfXi12lnyZDXNn/OEKz59u6+LuPBVgO/9CRyclHmdlvwg8c7PJ9eX2ZMnjWg=="], - - "@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.83.10", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@react-native/codegen": "0.83.10" } }, "sha512-iRfhxsOePloy12TUzpL9dCwnxLi3eurg8+JurSozAmWh4HtmKu0IJHNjbEKk9htI5n4RlFom8LQ9vVcvgWOTbw=="], - - "@react-native/babel-preset": ["@react-native/babel-preset@0.83.10", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-function-name": "^7.25.1", "@babel/plugin-transform-literals": "^7.25.2", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-shorthand-properties": "^7.24.7", "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", "@react-native/babel-plugin-codegen": "0.83.10", "babel-plugin-syntax-hermes-parser": "0.32.0", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-wTZWvs5cUQqr2qBAoJNKUy6IBH7wqPAJdxTC834S/7vBqtXypYHxDAFzYXkIh8pRTC86IBYxyc2IrBYHnB8O2g=="], - - "@react-native/codegen": ["@react-native/codegen@0.83.4", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.32.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" } }, "sha512-CJ7XutzIqJPz3Lp/5TOiRWlU/JAjTboMT1BHNLSXjYHXwTmgHM3iGEbpCOtBMjWvsojRTJyRO/G3ghInIIXEYg=="], - - "@react-native/community-cli-plugin": ["@react-native/community-cli-plugin@0.83.4", "", { "dependencies": { "@react-native/dev-middleware": "0.83.4", "debug": "^4.4.0", "invariant": "^2.2.4", "metro": "^0.83.3", "metro-config": "^0.83.3", "metro-core": "^0.83.3", "semver": "^7.1.3" }, "peerDependencies": { "@react-native-community/cli": "*", "@react-native/metro-config": "*" }, "optionalPeers": ["@react-native-community/cli", "@react-native/metro-config"] }, "sha512-8os0weQEnjUhWy7Db881+JKRwNHVGM40VtTRvltAyA/YYkrGg4kPCqiTybMxQDEcF3rnviuxHyI+ITiglfmgmQ=="], - - "@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.83.10", "", {}, "sha512-AlSOMdXoaSXi4O82f3APvw14e+EfrEvwPerfH2AI3JUrD/yQjFtbIxeyRPd89/MlKIbZU4cwyVFqj96bj39J5g=="], - - "@react-native/debugger-shell": ["@react-native/debugger-shell@0.83.10", "", { "dependencies": { "cross-spawn": "^7.0.6", "fb-dotslash": "0.5.8" } }, "sha512-hk9xFI9H412XSX1lpVuKrnxUQe3zIPKxFceYPUwx2L5aZQQ/yjypWkLs+LLldV6eh93B2sBnZbns1oFSE3LQNw=="], - - "@react-native/dev-middleware": ["@react-native/dev-middleware@0.83.10", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.83.10", "@react-native/debugger-shell": "0.83.10", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^4.4.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", "ws": "^7.5.10" } }, "sha512-V39TcESd9LGzDBs7PYVsx5oE1oGv1dLC0GIyJyEyehZjmOYk5sJ4C0m1ATKPeDE7JhiY8s/p6pNOBNp+NF4FGQ=="], - - "@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.83.4", "", {}, "sha512-AhaSWw2k3eMKqZ21IUdM7rpyTYOpAfsBbIIiom1QQii3QccX0uW2AWTcRhfuWRxqr2faGFaOBYedWl2fzp5hgw=="], - - "@react-native/js-polyfills": ["@react-native/js-polyfills@0.83.4", "", {}, "sha512-wYUdv0rt4MjhKhQloO1AnGDXhZQOFZHDxm86dEtEA0WcsCdVrFdRULFM+rKUC/QQtJW2rS6WBqtBusgtrsDADg=="], - - "@react-native/normalize-colors": ["@react-native/normalize-colors@0.83.4", "", {}, "sha512-9ezxaHjxqTkTOLg62SGg7YhFaE+fxa/jlrWP0nwf7eGFHlGOiTAaRR2KUfiN3K05e+EMbEhgcH/c7bgaXeGyJw=="], - - "@react-native/virtualized-lists": ["@react-native/virtualized-lists@0.83.4", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.2.0", "react": "*", "react-native": "*" }, "optionalPeers": ["@types/react"] }, "sha512-vNF/8kokMW8JEjG4n+j7veLTjHRRABlt4CaTS6+wtqzvWxCJHNIC8fhCqrDPn9fIn8sNePd8DyiFVX5L9TBBRA=="], - - "@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.18.14", "", { "dependencies": { "@react-navigation/elements": "^2.9.36", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { "@react-navigation/native": "^7.3.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-A3V9rDSut459TBPtkD7rb0npUUBlJBfMunyRT5nOGKqPguhuXWk1h91NfQMuqyW2DCUvvFZChzsbLbj42rXTdQ=="], - - "@react-navigation/core": ["@react-navigation/core@7.21.11", "", { "dependencies": { "@react-navigation/routers": "^7.6.4", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "query-string": "^7.1.3", "react-is": "^19.1.0", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": ">= 18.2.0" } }, "sha512-bCW1PsLA/eOXDOukcJFEzlcL3Zpy8DJuDCfkDDwAQlAgoSZ/J9+ZeDRUMmCUi6xbnFgvFEEIMertaLeErOFP0Q=="], - - "@react-navigation/elements": ["@react-navigation/elements@2.9.36", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.3.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-+10x9s5v2Q7FwAYdSmPMgILtxZyC5e4hWJQu8g5o3u4p8DUToTBmGvys/UvmEr+h9xmm0Go42qw9Ff2ape53kQ=="], - - "@react-navigation/native": ["@react-navigation/native@7.3.14", "", { "dependencies": { "@react-navigation/core": "^7.21.11", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "standard-navigation": "^0.0.8", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*" } }, "sha512-hcKTDNBuuAA1/xW6QeKYmMPVhk5W9dKGQpPmn5dQeeePwMpu5OZ14NOgwKH0w9D3tg2jupojTcVL0tsx5DTFXg=="], - - "@react-navigation/native-stack": ["@react-navigation/native-stack@7.18.6", "", { "dependencies": { "@react-navigation/elements": "^2.9.36", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0", "warn-once": "^0.1.1" }, "peerDependencies": { "@react-navigation/native": "^7.3.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-KuvvSBddHrbKC4c6yKz+UCFey2xgTuPQHgjf2wJQAvA7JM8jCQa3G1+QzhO6d44nYNKir3y6EounXZvQE/BX6w=="], - - "@react-navigation/routers": ["@react-navigation/routers@7.6.4", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-GI7eJm8/KsZUQaYcXvEExikKurRZRgEsSzyZ7faENfi65yqJBCXjDMwyN1pF6pNW1MoLH1ErDwDivFxY6BzD3w=="], - "@reduxjs/toolkit": ["@reduxjs/toolkit@2.12.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw=="], "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="], @@ -2167,12 +1794,6 @@ "@rrweb/utils": ["@rrweb/utils@2.1.1", "", {}, "sha512-x2SgJAD3YJ9eVcLZ5l6OqWMtczdA3VfS5XqPeqpZdQgV9oh6Id5GK2cDN4bimnkqryOcIJdz8cTvV0yNvqQ+nA=="], - "@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], - - "@scure/bip32": ["@scure/bip32@1.7.0", "", { "dependencies": { "@noble/curves": "~1.9.0", "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw=="], - - "@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], - "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], "@shadcn/react": ["@shadcn/react@0.2.1", "", { "peerDependencies": { "@types/react": ">=19", "react": ">=19" }, "optionalPeers": ["@types/react", "react"] }, "sha512-5krgi3dRMKb5jH6a+qPzVJUy/54s0kKE4Rw4LjDfLqOdVQTWKUgxWf1kW8r912I0jX/Lzxqc+pgjkjWxUIK5BQ=="], @@ -2193,16 +1814,12 @@ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], - "@sinclair/typebox": ["@sinclair/typebox@0.27.12", "", {}, "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g=="], + "@sinclair/typebox": ["@sinclair/typebox@0.31.30", "", {}, "sha512-MGsM7bVmHg3sUKCphlu3SGQ+T+5JTbygZWYArfKQCW5anJeACHeqLhcnf+R7XlCWZ+QR3C8JTBx3kCKJTN69ow=="], "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], - "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="], - - "@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], - "@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-QfuLWAkLzptffFW980AFeHZFdqds2B64rpEd3uJ6lgs3xVn9QegGMUgUcj+4d7dRrAsya3r58ZKpku97WcFb4w=="], @@ -2231,122 +1848,6 @@ "@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@solana-mobile/mobile-wallet-adapter-protocol": ["@solana-mobile/mobile-wallet-adapter-protocol@2.2.9", "", { "dependencies": { "@solana/kit": "^6.0.0", "@solana/wallet-standard-features": "^1.3.0", "@solana/wallet-standard-util": "^1.1.2", "@wallet-standard/core": "^1.1.1" }, "peerDependencies": { "react-native": ">0.74" } }, "sha512-qtWJq0hzKgngVG0So2ViBRPYDculaxgj40WDSP1nzgdg85MXyAmTEjQqV10uSVbZRm9b1hl601HDJX1sHTgeiw=="], - - "@solana-mobile/mobile-wallet-adapter-protocol-web3js": ["@solana-mobile/mobile-wallet-adapter-protocol-web3js@2.2.9", "", { "dependencies": { "@solana-mobile/mobile-wallet-adapter-protocol": "^2.2.9" }, "peerDependencies": { "@solana/web3.js": "^1.98.4" } }, "sha512-TAMItAuOb7duwYQ+PZ6JNqw/6TEhCWLObHF5uOBGV9tjCozHGVunxa7lTeuHAWIgO68IAo86MZdE5kethDATpw=="], - - "@solana-mobile/wallet-adapter-mobile": ["@solana-mobile/wallet-adapter-mobile@2.2.9", "", { "dependencies": { "@solana-mobile/mobile-wallet-adapter-protocol": "^2.2.9", "@solana-mobile/mobile-wallet-adapter-protocol-web3js": "^2.2.9", "@solana-mobile/wallet-standard-mobile": "^0.5.3", "@solana/wallet-adapter-base": "^0.9.27", "@solana/wallet-standard-features": "^1.3.0", "@wallet-standard/core": "^1.1.1", "tslib": "^2.8.1" }, "optionalDependencies": { "@react-native-async-storage/async-storage": "^1.17.7" }, "peerDependencies": { "@solana/web3.js": "^1.98.4", "react-native": ">0.74" } }, "sha512-IWzI2pUmEqBsTNo+XqMTY38NL5TSdMetES59N0Cmuf+zYqiS56rHdMwD/R2IOy7E4yKa1CsuHfCh6CE6zk4ViQ=="], - - "@solana-mobile/wallet-standard-mobile": ["@solana-mobile/wallet-standard-mobile@0.5.3", "", { "dependencies": { "@solana-mobile/mobile-wallet-adapter-protocol": "^2.2.9", "@solana/wallet-standard-chains": "^1.1.1", "@solana/wallet-standard-features": "^1.3.0", "@wallet-standard/base": "^1.0.1", "@wallet-standard/features": "^1.0.3", "@wallet-standard/wallet": "^1.1.0", "qrcode": "^1.5.4", "tslib": "^2.8.1" }, "optionalDependencies": { "@react-native-async-storage/async-storage": "^1.17.7" } }, "sha512-/Ea/CNIWHExpXsA6syVy7fUUhONtYob+VMsmF8flm8GEAZQyzX6UtKSy4NQMMTGBTAGlmmmbBVrZF+Tp5/fDxQ=="], - - "@solana/accounts": ["@solana/accounts@6.10.0", "", { "dependencies": { "@solana/addresses": "6.10.0", "@solana/codecs-core": "6.10.0", "@solana/codecs-strings": "6.10.0", "@solana/errors": "6.10.0", "@solana/rpc-spec": "6.10.0", "@solana/rpc-types": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-+FxfDOrnifoPlBkF+fr8eeQdgM6xtIgAg9xKMu3WnIz60oZd4Xnry6+ff6t+ePPoZZp397FSg9ZJet68VCWm5Q=="], - - "@solana/addresses": ["@solana/addresses@6.10.0", "", { "dependencies": { "@solana/assertions": "6.10.0", "@solana/codecs-core": "6.10.0", "@solana/codecs-strings": "6.10.0", "@solana/errors": "6.10.0", "@solana/nominal-types": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-vEoCGBTxG0HCERAn84KXkrJjl+pDaNzOpZ0qbgcPS98fYxP5yzbKB8SNOY2bzrbkRUmmw5Q3hqTRERemUN2Gcw=="], - - "@solana/assertions": ["@solana/assertions@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-lKSAdVo+P/6Lp4vs6shstXmFOpvxrABwn4o1462tb7sKkNapk6o9pPFVPGw4DUgPS3WqWRs1j2tmpuVjhQRntg=="], - - "@solana/buffer-layout": ["@solana/buffer-layout@4.0.1", "", { "dependencies": { "buffer": "~6.0.3" } }, "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA=="], - - "@solana/codecs": ["@solana/codecs@6.10.0", "", { "dependencies": { "@solana/codecs-core": "6.10.0", "@solana/codecs-data-structures": "6.10.0", "@solana/codecs-numbers": "6.10.0", "@solana/codecs-strings": "6.10.0", "@solana/fixed-points": "6.10.0", "@solana/options": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-lLVuxod4ChWp9i7OvpgIykYG8Q9OGPVXKnHM9VlzDDLylsx7Y1FoQL00sHa7PqFkJVmkBufaA6dcGbQ7FU+lAQ=="], - - "@solana/codecs-core": ["@solana/codecs-core@2.3.0", "", { "dependencies": { "@solana/errors": "2.3.0" }, "peerDependencies": { "typescript": ">=5.3.3" } }, "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw=="], - - "@solana/codecs-data-structures": ["@solana/codecs-data-structures@6.10.0", "", { "dependencies": { "@solana/codecs-core": "6.10.0", "@solana/codecs-numbers": "6.10.0", "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-CNasJW3bq5u+632Zt5aJ8rOjAjv2HyenpV8o9kAIqdmV4CBpjCCoBnKn8LkuR/sbeREZxJYfhKTXO/9ruAkw7A=="], - - "@solana/codecs-numbers": ["@solana/codecs-numbers@2.3.0", "", { "dependencies": { "@solana/codecs-core": "2.3.0", "@solana/errors": "2.3.0" }, "peerDependencies": { "typescript": ">=5.3.3" } }, "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg=="], - - "@solana/codecs-strings": ["@solana/codecs-strings@6.10.0", "", { "dependencies": { "@solana/codecs-core": "6.10.0", "@solana/codecs-numbers": "6.10.0", "@solana/errors": "6.10.0" }, "peerDependencies": { "fastestsmallesttextencoderdecoder": "^1.0.22", "typescript": ">=5.4.0" }, "optionalPeers": ["fastestsmallesttextencoderdecoder", "typescript"] }, "sha512-zlaqkg7K6F6IN4V/Ec8TWkTn054gxv7ZLagvGkuEyAdPQ6BzzsehOm2TqCuyXgJJTCGPLY1bEk6yH9NxANe0kA=="], - - "@solana/errors": ["@solana/errors@2.3.0", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^14.0.0" }, "peerDependencies": { "typescript": ">=5.3.3" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ=="], - - "@solana/fast-stable-stringify": ["@solana/fast-stable-stringify@6.10.0", "", { "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-iCNed27wk6PKSS3QUtHovRfMWF/jbVWogs2vB4tukKUCsqG4rDfDInIwZ6ur/nY6XTrgi2gMMdZq9GAUlWsbfw=="], - - "@solana/fixed-points": ["@solana/fixed-points@6.10.0", "", { "dependencies": { "@solana/codecs-core": "6.10.0", "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-ZkKL0alXH3L7/wMiVG8YUuG8qBKunlM810+YBD7nUPRhifiGsX1zwADViHLYNqLr/jUk0mTYFUcKznTpB/K+Gg=="], - - "@solana/functional": ["@solana/functional@6.10.0", "", { "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-P8cevu4mAqHTXC37h1TVoOh8zhWB2tlOI/R9vWjYPpcLwcyWf8p2qq4LEGHl5kY+1C+4PNX39HsmCocXOPCDkQ=="], - - "@solana/instruction-plans": ["@solana/instruction-plans@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0", "@solana/instructions": "6.10.0", "@solana/keys": "6.10.0", "@solana/promises": "6.10.0", "@solana/transaction-messages": "6.10.0", "@solana/transactions": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-YG7mo4zykzdc6ZTV0BuN6pveK9qeBySzlYYerq578A4eQu3xcypMAYRGAvhMZtWTanjjmD6CKtM0M7kVp0TNxg=="], - - "@solana/instructions": ["@solana/instructions@6.10.0", "", { "dependencies": { "@solana/codecs-core": "6.10.0", "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-0TToYF+8LXQ3ofPMx+yF6yaM9l4YJvcAPMy0qV5JsrBUFlWXBSANRuudKBQLHMvb+a3OiUTq5X7omuorKMBB3A=="], - - "@solana/keys": ["@solana/keys@6.10.0", "", { "dependencies": { "@solana/assertions": "6.10.0", "@solana/codecs-core": "6.10.0", "@solana/codecs-strings": "6.10.0", "@solana/errors": "6.10.0", "@solana/nominal-types": "6.10.0", "@solana/promises": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-26IRfdm/hTUCmM7MeEeX0ULSbCM6OzkZTkfkrPircqmRM7xyNqP4hq7u0P7wjb9dl7NfgyG6K7cdvUxrj2e3mA=="], - - "@solana/kit": ["@solana/kit@6.10.0", "", { "dependencies": { "@solana/accounts": "6.10.0", "@solana/addresses": "6.10.0", "@solana/codecs": "6.10.0", "@solana/errors": "6.10.0", "@solana/functional": "6.10.0", "@solana/instruction-plans": "6.10.0", "@solana/instructions": "6.10.0", "@solana/keys": "6.10.0", "@solana/offchain-messages": "6.10.0", "@solana/plugin-core": "6.10.0", "@solana/plugin-interfaces": "6.10.0", "@solana/program-client-core": "6.10.0", "@solana/programs": "6.10.0", "@solana/rpc": "6.10.0", "@solana/rpc-api": "6.10.0", "@solana/rpc-parsed-types": "6.10.0", "@solana/rpc-spec-types": "6.10.0", "@solana/rpc-subscriptions": "6.10.0", "@solana/rpc-types": "6.10.0", "@solana/signers": "6.10.0", "@solana/subscribable": "6.10.0", "@solana/sysvars": "6.10.0", "@solana/transaction-confirmation": "6.10.0", "@solana/transaction-messages": "6.10.0", "@solana/transactions": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-/WnnQp3uARh2JCFSfAakejTAqwmXVuMVTcRn5r2yDwY2yzZ4R6mt/Cl59VPimVLNSoTyN/KsEwhv9omr3ERazQ=="], - - "@solana/nominal-types": ["@solana/nominal-types@6.10.0", "", { "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-9ykyBBvnkInH7fCacjJi7zu2PJyd+OCt+VTjIISv070fHzKIMFqZqJJ/dJ0SRH2aHwfB3n86iVsmtBtuxi4KKA=="], - - "@solana/offchain-messages": ["@solana/offchain-messages@6.10.0", "", { "dependencies": { "@solana/addresses": "6.10.0", "@solana/codecs-core": "6.10.0", "@solana/codecs-data-structures": "6.10.0", "@solana/codecs-numbers": "6.10.0", "@solana/codecs-strings": "6.10.0", "@solana/errors": "6.10.0", "@solana/keys": "6.10.0", "@solana/nominal-types": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-RiEgAueeMkFMC1suOXBIcmCZgtXRxy24yk0DldPB37bB4zwOF1SAaRjNRPjIkGK8RhCYrEpPosnzLyavw9ueRg=="], - - "@solana/options": ["@solana/options@6.10.0", "", { "dependencies": { "@solana/codecs-core": "6.10.0", "@solana/codecs-data-structures": "6.10.0", "@solana/codecs-numbers": "6.10.0", "@solana/codecs-strings": "6.10.0", "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-RO9UT3UYD8/Cu2uM6ZXbKvLeMnVD42+g9JRds7Pfs4AhiOyg4R4TJrQUAppTgavPTO3PBRlWtWOC05ZH/yAIbg=="], - - "@solana/plugin-core": ["@solana/plugin-core@6.10.0", "", { "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-JE70YTQOfFACVFGvoJon4Scc/eHUWjMu8Ovo35CcV2kHTAHYMCd4UkBd2gmlhK0vRMMomsQi1ZLPlAlTq0OoUQ=="], - - "@solana/plugin-interfaces": ["@solana/plugin-interfaces@6.10.0", "", { "dependencies": { "@solana/addresses": "6.10.0", "@solana/instruction-plans": "6.10.0", "@solana/keys": "6.10.0", "@solana/rpc-spec": "6.10.0", "@solana/rpc-subscriptions-spec": "6.10.0", "@solana/rpc-types": "6.10.0", "@solana/signers": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-vr0/l9wcM4orwGr8cjkFWaJ9A4HvzuAv00jMFNMg0Spd0GZqnwnpW+D/fXa1lIJnTRaF3EeEjLh4VjKU037T0Q=="], - - "@solana/program-client-core": ["@solana/program-client-core@6.10.0", "", { "dependencies": { "@solana/accounts": "6.10.0", "@solana/addresses": "6.10.0", "@solana/codecs-core": "6.10.0", "@solana/errors": "6.10.0", "@solana/instruction-plans": "6.10.0", "@solana/instructions": "6.10.0", "@solana/plugin-interfaces": "6.10.0", "@solana/rpc-api": "6.10.0", "@solana/signers": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-4PPbTLdC1ylHIuvhOFDP8RnSkXPCFjNFWGslzc+UFKnoR4ajzBcByX94jmaruDMk5ncxgj7tr9pzJTvfGHIaMA=="], - - "@solana/programs": ["@solana/programs@6.10.0", "", { "dependencies": { "@solana/addresses": "6.10.0", "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-qn/HeLP5KGUJXVub3fyGe69/rWaLX4jzwm6V/1pNxJDbdF+MBdgn18hP6F+VmhfdNmwK0lue3J/1HQ1UTMuQeQ=="], - - "@solana/promises": ["@solana/promises@6.10.0", "", { "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-oJSIn+VBBMWDo8oqw7RV3tI6Jih+Ieup6FcQLYLDUriaeo7+8l1Zdezl8zh7SIfeU4lOfAbRg6mR0huaS/Lltg=="], - - "@solana/rpc": ["@solana/rpc@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0", "@solana/fast-stable-stringify": "6.10.0", "@solana/functional": "6.10.0", "@solana/rpc-api": "6.10.0", "@solana/rpc-spec": "6.10.0", "@solana/rpc-spec-types": "6.10.0", "@solana/rpc-transformers": "6.10.0", "@solana/rpc-transport-http": "6.10.0", "@solana/rpc-types": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-EwxsqoD+NXV+m+iobnWNtATD93gTgaNsOiQOzYB1/2e+8S6fl6obdNPB55yfXgtl4jt6GV6/ae4xuPhLv76vvg=="], - - "@solana/rpc-api": ["@solana/rpc-api@6.10.0", "", { "dependencies": { "@solana/addresses": "6.10.0", "@solana/codecs-core": "6.10.0", "@solana/codecs-strings": "6.10.0", "@solana/errors": "6.10.0", "@solana/keys": "6.10.0", "@solana/rpc-parsed-types": "6.10.0", "@solana/rpc-spec": "6.10.0", "@solana/rpc-transformers": "6.10.0", "@solana/rpc-types": "6.10.0", "@solana/transaction-messages": "6.10.0", "@solana/transactions": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-RjPIVsAb/85P1ptoO3WpC0x7QG6gG/e4q/3lo6gbSznUZOcoM+8sSBnCX7BwP1ZkCDS6NK/ClXLnhhhYZx+OGg=="], - - "@solana/rpc-parsed-types": ["@solana/rpc-parsed-types@6.10.0", "", { "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-5275mvSV1mxhwvrMVa+K7BU/nAetpHfcb+8Ql9rtA8RRf6DyiimFQFZUukE4Ez6XJihEpCHNy98yhkgai9wytQ=="], - - "@solana/rpc-spec": ["@solana/rpc-spec@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0", "@solana/rpc-spec-types": "6.10.0", "@solana/subscribable": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-yQdbWw5mZEWrwsunHR9NHkuhMXIB9sPOObwm18D53v5tAJnxTB0IcHvO647XqFDLTK/yQ4AdDtlYD1vsY07AMQ=="], - - "@solana/rpc-spec-types": ["@solana/rpc-spec-types@6.10.0", "", { "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-NDZrKyZrJk4HaMFhTE/lAiMB824cWAodKqDHyKi0UteHU9pyRmil3BN1jt7e+j08mwMWwfklSgyrTaq52g6DIQ=="], - - "@solana/rpc-subscriptions": ["@solana/rpc-subscriptions@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0", "@solana/fast-stable-stringify": "6.10.0", "@solana/functional": "6.10.0", "@solana/promises": "6.10.0", "@solana/rpc-spec-types": "6.10.0", "@solana/rpc-subscriptions-api": "6.10.0", "@solana/rpc-subscriptions-channel-websocket": "6.10.0", "@solana/rpc-subscriptions-spec": "6.10.0", "@solana/rpc-transformers": "6.10.0", "@solana/rpc-types": "6.10.0", "@solana/subscribable": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-6mfuHp/K7unFKCOTCCBC9ziEGnxe2tyJ74EbR51QUnBeCUdYD7Hhdpxic1WRSJ3UeNW/mG4OzFM6z8Wi64Eh9Q=="], - - "@solana/rpc-subscriptions-api": ["@solana/rpc-subscriptions-api@6.10.0", "", { "dependencies": { "@solana/addresses": "6.10.0", "@solana/keys": "6.10.0", "@solana/rpc-subscriptions-spec": "6.10.0", "@solana/rpc-transformers": "6.10.0", "@solana/rpc-types": "6.10.0", "@solana/transaction-messages": "6.10.0", "@solana/transactions": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-CRPQoTtT1cOwOQUsqS7jgo7wYdAj7jB5ab/UmMPWVpecf2FNMhWhgvxP2s82M7VkDGTGl13qaQ0WySmi7Egrlg=="], - - "@solana/rpc-subscriptions-channel-websocket": ["@solana/rpc-subscriptions-channel-websocket@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0", "@solana/functional": "6.10.0", "@solana/rpc-subscriptions-spec": "6.10.0", "@solana/subscribable": "6.10.0", "ws": "^8.21.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-KkqP1186HELPlJftA88SNAT2znR8knCVzsUipXVzY4zfW8sN3LOa0ePMzh9VZ/V+J+raTt55laR87ovAO0n+zw=="], - - "@solana/rpc-subscriptions-spec": ["@solana/rpc-subscriptions-spec@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0", "@solana/promises": "6.10.0", "@solana/rpc-spec-types": "6.10.0", "@solana/subscribable": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-nWMwGaG4ulzeX2sskY5TywXF3cwEd8FDmUpLe2JBWxE8XDAOGOKcsYPYFcBgb8ee9KqfPT2PTNdcz9jOhJf34w=="], - - "@solana/rpc-transformers": ["@solana/rpc-transformers@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0", "@solana/functional": "6.10.0", "@solana/nominal-types": "6.10.0", "@solana/rpc-spec-types": "6.10.0", "@solana/rpc-types": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-2nFUrVTiE720pJOY4XKx3HuYmishw0of/4oScu76YGm6O8wsmvFvPNAkrEinmieWXQkfpBfRvLZmpl8PaAy+ug=="], - - "@solana/rpc-transport-http": ["@solana/rpc-transport-http@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0", "@solana/rpc-spec": "6.10.0", "@solana/rpc-spec-types": "6.10.0", "undici-types": "^8.4.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-JrdNuYi0nBbD3X8JUtgX1dQJwIwz/WJvmigDdELysXfGB2bTJpfjqGDLhCLOz2sRl66FASIEqgG/LVa2C9VXcA=="], - - "@solana/rpc-types": ["@solana/rpc-types@6.10.0", "", { "dependencies": { "@solana/addresses": "6.10.0", "@solana/codecs-core": "6.10.0", "@solana/codecs-numbers": "6.10.0", "@solana/codecs-strings": "6.10.0", "@solana/errors": "6.10.0", "@solana/fixed-points": "6.10.0", "@solana/nominal-types": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-zaSecTfCPvz/vcoAmKD6XoRstGHTr1EKJBD8T9UcpEFFB6CtF6DxerDB+wrzkamuT6msmnR2DWXMrYOGDAsgIg=="], - - "@solana/signers": ["@solana/signers@6.10.0", "", { "dependencies": { "@solana/addresses": "6.10.0", "@solana/codecs-core": "6.10.0", "@solana/errors": "6.10.0", "@solana/instructions": "6.10.0", "@solana/keys": "6.10.0", "@solana/nominal-types": "6.10.0", "@solana/offchain-messages": "6.10.0", "@solana/transaction-messages": "6.10.0", "@solana/transactions": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-+vtCc+mT1FpGxrA5oL2aaMxSHiMJ2hH5PcDIfjo2XJkHz2klZiCZyT5F9+zpltc9vdi1QTElQq59Sfplmtd33A=="], - - "@solana/subscribable": ["@solana/subscribable@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0", "@solana/promises": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-VsR6XMwkiDBkZJUcoGkEOhf397pOV75gKCL9Bx8bpi2T3Bbs0CxUpMn4yaUgAnRba3eXmjbXMNCXjttfa6sKbw=="], - - "@solana/sysvars": ["@solana/sysvars@6.10.0", "", { "dependencies": { "@solana/accounts": "6.10.0", "@solana/codecs-core": "6.10.0", "@solana/codecs-data-structures": "6.10.0", "@solana/codecs-numbers": "6.10.0", "@solana/errors": "6.10.0", "@solana/rpc-types": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-cG13p1+onxz+20iWjwWQr1Z1jQwPm0fnjoW75fqZq7p4rVCie3L2sXvaJsYPjWKrUvpOzOIEHnqZGkG05rCpjg=="], - - "@solana/transaction-confirmation": ["@solana/transaction-confirmation@6.10.0", "", { "dependencies": { "@solana/addresses": "6.10.0", "@solana/codecs-strings": "6.10.0", "@solana/errors": "6.10.0", "@solana/keys": "6.10.0", "@solana/promises": "6.10.0", "@solana/rpc": "6.10.0", "@solana/rpc-subscriptions": "6.10.0", "@solana/rpc-types": "6.10.0", "@solana/transaction-messages": "6.10.0", "@solana/transactions": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-ULvtg65qfenh4T/GYcIlKSUv5EqDcng9UN0dxbHU4kuZdR2e0B8HN2xDC4WhcFQVeFJSbTZmaYFkeTY/Y4gfGQ=="], - - "@solana/transaction-messages": ["@solana/transaction-messages@6.10.0", "", { "dependencies": { "@solana/addresses": "6.10.0", "@solana/codecs-core": "6.10.0", "@solana/codecs-data-structures": "6.10.0", "@solana/codecs-numbers": "6.10.0", "@solana/errors": "6.10.0", "@solana/functional": "6.10.0", "@solana/instructions": "6.10.0", "@solana/nominal-types": "6.10.0", "@solana/rpc-types": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-s7v8G3BTxGlKYIj3eWCG0g1296v+1LBt16mVnlRH5FuyaJ5AdhlhtRho5HUDpdwE8EXun+y1c48V6uhcZ8wdbQ=="], - - "@solana/transactions": ["@solana/transactions@6.10.0", "", { "dependencies": { "@solana/addresses": "6.10.0", "@solana/codecs-core": "6.10.0", "@solana/codecs-data-structures": "6.10.0", "@solana/codecs-numbers": "6.10.0", "@solana/codecs-strings": "6.10.0", "@solana/errors": "6.10.0", "@solana/functional": "6.10.0", "@solana/instructions": "6.10.0", "@solana/keys": "6.10.0", "@solana/nominal-types": "6.10.0", "@solana/rpc-types": "6.10.0", "@solana/transaction-messages": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-VADSqP9OTYmhrox4pcgDd4+RjVmednXSE0+8Y7SPK4PN1pK5Az2RJ0nSsy0xcTnaOr8mF/crwFktqPrRQwSbQA=="], - - "@solana/wallet-adapter-base": ["@solana/wallet-adapter-base@0.9.27", "", { "dependencies": { "@solana/wallet-standard-features": "^1.3.0", "@wallet-standard/base": "^1.1.0", "@wallet-standard/features": "^1.1.0", "eventemitter3": "^5.0.1" }, "peerDependencies": { "@solana/web3.js": "^1.98.0" } }, "sha512-kXjeNfNFVs/NE9GPmysBRKQ/nf+foSaq3kfVSeMcO/iVgigyRmB551OjU3WyAolLG/1jeEfKLqF9fKwMCRkUqg=="], - - "@solana/wallet-adapter-react": ["@solana/wallet-adapter-react@0.15.39", "", { "dependencies": { "@solana-mobile/wallet-adapter-mobile": "^2.2.0", "@solana/wallet-adapter-base": "^0.9.27", "@solana/wallet-standard-wallet-adapter-react": "^1.1.4" }, "peerDependencies": { "@solana/web3.js": "^1.98.0", "react": "*" } }, "sha512-WXtlo88ith5m22qB+qiGw301/Zb9r5pYr4QdXWmlXnRNqwST5MGmJWhG+/RVrzc+OG7kSb3z1gkVNv+2X/Y0Gg=="], - - "@solana/wallet-standard": ["@solana/wallet-standard@1.1.4", "", { "dependencies": { "@solana/wallet-standard-core": "^1.1.2", "@solana/wallet-standard-wallet-adapter": "^1.1.4" } }, "sha512-NF+MI5tOxyvfTU4A+O5idh/gJFmjm52bMwsPpFGRSL79GECSN0XLmpVOO/jqTKJgac2uIeYDpQw/eMaQuWuUXw=="], - - "@solana/wallet-standard-chains": ["@solana/wallet-standard-chains@1.1.2", "", { "dependencies": { "@wallet-standard/base": "^1.1.0" } }, "sha512-EZobEGclDBAFplpJC5F3d/s8Xnlqc5isNKuPrd5o9ZPZ7tWN84O0e68yIZ8MAOj9V7ieRadNiHtql7uIXCTyXg=="], - - "@solana/wallet-standard-core": ["@solana/wallet-standard-core@1.1.3", "", { "dependencies": { "@solana/wallet-standard-chains": "^1.1.2", "@solana/wallet-standard-features": "^1.4.0", "@solana/wallet-standard-util": "^1.1.3" } }, "sha512-ZD0seJHb3A7QWOlwMECzq3vJR1Dg75+ZoW68lMyO1UfqH+AHMx0j5B6jdjbH3PFoqauU2q3RexWMcD4Jc7D9xA=="], - - "@solana/wallet-standard-features": ["@solana/wallet-standard-features@1.4.0", "", { "dependencies": { "@wallet-standard/base": "^1.1.0", "@wallet-standard/features": "^1.1.0" } }, "sha512-f0tAdqwM2aL6CiFbIgt9h5zKFp+mgY/iNGwoxPMTj9VSTeQj7d1GGSmWhZw0XWoZ4N/1tnKTKmYFq+Dyq08jRw=="], - - "@solana/wallet-standard-util": ["@solana/wallet-standard-util@1.1.3", "", { "dependencies": { "@noble/curves": "^1.8.2", "@solana/wallet-standard-chains": "^1.1.2", "@solana/wallet-standard-features": "^1.4.0" } }, "sha512-aweR5y5FjYaeS9TkoqAWERFpGUj2MJepsDhcekCuoPLcNCquJL85Nsnuy01tBybspN5+Y09SWkxwsODOFGSfkg=="], - - "@solana/wallet-standard-wallet-adapter": ["@solana/wallet-standard-wallet-adapter@1.1.5", "", { "dependencies": { "@solana/wallet-standard-wallet-adapter-base": "^1.1.5", "@solana/wallet-standard-wallet-adapter-react": "^1.1.5" } }, "sha512-6EMRyXzLcXQY9P5Bo8XWh8It6lb4rUbwO+RDdSpEySz/v+ric0W9H3Yzlqted4AIXQMJgxZifgyFxTp6g/bfZA=="], - - "@solana/wallet-standard-wallet-adapter-base": ["@solana/wallet-standard-wallet-adapter-base@1.1.5", "", { "dependencies": { "@solana/wallet-adapter-base": "^0.9.24", "@solana/wallet-standard-chains": "^1.1.2", "@solana/wallet-standard-features": "^1.4.0", "@solana/wallet-standard-util": "^1.1.3", "@wallet-standard/app": "^1.1.0", "@wallet-standard/base": "^1.1.0", "@wallet-standard/features": "^1.1.0", "@wallet-standard/wallet": "^1.1.0" }, "peerDependencies": { "@solana/web3.js": "^1.98.0", "bs58": "^6.0.0" } }, "sha512-dbk+4mJAsZ1a2R/v5/Qvp6SleviuSNWd9LnuQ0ekH0HRRJTOWyTBJsdQsVDdAymdPnLAaIN5J10ni1CT2Z+ERg=="], - - "@solana/wallet-standard-wallet-adapter-react": ["@solana/wallet-standard-wallet-adapter-react@1.1.5", "", { "dependencies": { "@solana/wallet-standard-wallet-adapter-base": "^1.1.5", "@wallet-standard/app": "^1.1.0", "@wallet-standard/base": "^1.1.0" }, "peerDependencies": { "@solana/wallet-adapter-base": "*", "react": "*" } }, "sha512-JfKBU2Mc332pR+9xhxjr5U/Q2aL03mMmSbrcnvfvAjML6zUEzAQ/bV6DchRsdtAT8Rx0zEg8h4OhsXbmteu0Yg=="], - - "@solana/web3.js": ["@solana/web3.js@1.98.4", "", { "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", "@noble/hashes": "^1.4.0", "@solana/buffer-layout": "^4.0.1", "@solana/codecs-numbers": "^2.1.0", "agentkeepalive": "^4.5.0", "bn.js": "^5.2.1", "borsh": "^0.7.0", "bs58": "^4.0.1", "buffer": "6.0.3", "fast-stable-stringify": "^1.0.0", "jayson": "^4.1.1", "node-fetch": "^2.7.0", "rpc-websockets": "^9.0.2", "superstruct": "^2.0.2" } }, "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw=="], - "@speed-highlight/core": ["@speed-highlight/core@1.2.17", "", {}, "sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg=="], "@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="], @@ -2363,8 +1864,6 @@ "@streamdown/mermaid": ["@streamdown/mermaid@1.0.2", "", { "dependencies": { "mermaid": "^11.12.2" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-Fr/4sBWnAeSnxM3PcrV/+DiZe5oPMq9gOkUIAH7ZauJeuwrZ/DVzD4g0zlav6AH0axh2m/sOfrfLtY5aLT7niw=="], - "@stripe/stripe-js": ["@stripe/stripe-js@5.6.0", "", {}, "sha512-w8CEY73X/7tw2KKlL3iOk679V9bWseE4GzNz3zlaYxcTjmcmWOathRb0emgo/QQ3eoNzmq68+2Y2gxluAv3xGw=="], - "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], "@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="], @@ -2495,8 +1994,6 @@ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], - "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], - "@types/css-font-loading-module": ["@types/css-font-loading-module@0.0.7", "", {}, "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q=="], "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], @@ -2571,18 +2068,8 @@ "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], - "@types/graceful-fs": ["@types/graceful-fs@4.1.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ=="], - - "@types/hammerjs": ["@types/hammerjs@2.0.46", "", {}, "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw=="], - "@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="], - "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="], - - "@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="], - - "@types/istanbul-reports": ["@types/istanbul-reports@3.0.4", "", { "dependencies": { "@types/istanbul-lib-report": "*" } }, "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ=="], - "@types/jsesc": ["@types/jsesc@2.5.1", "", {}, "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw=="], "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="], @@ -2617,8 +2104,6 @@ "@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="], - "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], - "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], @@ -2629,10 +2114,6 @@ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@types/yargs": ["@types/yargs@17.0.35", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg=="], - - "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], - "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], @@ -2741,31 +2222,17 @@ "@vueuse/shared": ["@vueuse/shared@14.4.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g=="], - "@wallet-standard/app": ["@wallet-standard/app@1.1.1", "", { "dependencies": { "@wallet-standard/base": "^1.1.1" } }, "sha512-WDGwoByhP5gwHH01r5EaLgQdLVkACPCdOMQhmhn8rsm10h/siSgTorShzBxrn0ExSPof+Lu+C3TfgqBrPa1xoQ=="], + "@xstate/fsm": ["@xstate/fsm@1.6.5", "", {}, "sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw=="], - "@wallet-standard/base": ["@wallet-standard/base@1.1.1", "", {}, "sha512-gggIHTtxicF9XFMQ12DkfS6NAG92Ak795JeSA7f2whAQ6Y3AkMWWuCMxSZXG2NIPN42kEaZSNVjqMsJRaJRxMQ=="], + "@xyflow/react": ["@xyflow/react@12.11.2", "", { "dependencies": { "@xyflow/system": "0.0.79", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "@types/react": ">=17", "@types/react-dom": ">=17", "react": ">=17", "react-dom": ">=17" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-eLAlDWJfWnQEhJwGMjlWdAXO9eYllKpliUmPQlAmOLxz6mExXuzMVDUKLMquixgkrtmMFFtug3jGKmYYld12cA=="], - "@wallet-standard/core": ["@wallet-standard/core@1.1.1", "", { "dependencies": { "@wallet-standard/app": "^1.1.0", "@wallet-standard/base": "^1.1.0", "@wallet-standard/errors": "^0.1.1", "@wallet-standard/features": "^1.1.0", "@wallet-standard/wallet": "^1.1.0" } }, "sha512-5Xmjc6+Oe0hcPfVc5n8F77NVLwx1JVAoCVgQpLyv/43/bhtIif+Gx3WUrDlaSDoM8i2kA2xd6YoFbHCxs+e0zA=="], + "@xyflow/system": ["@xyflow/system@0.0.79", "", { "dependencies": { "@types/d3-drag": "^3.0.7", "@types/d3-interpolate": "^3.0.4", "@types/d3-selection": "^3.0.10", "@types/d3-transition": "^3.0.8", "@types/d3-zoom": "^3.0.8", "d3-drag": "^3.0.0", "d3-interpolate": "^3.0.1", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0" } }, "sha512-czLyOh91NF0hIzbNzwi8I6GlqG23BHh2435OddfI6uiaLH3xdrdygO93gqgH1Bv9mhy8XPFQJOBn1FTq4LvEWA=="], - "@wallet-standard/errors": ["@wallet-standard/errors@0.1.2", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^13.1.0" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-oEzKUqJefKby6wcIvaJgrSEe/uNn/rnqkJ0P/85K+h0i5Tdo9E3L22VWq/j5K1e8hHMnZd6LgaIr8m/Wn7X/Ng=="], + "@yuku-codegen/binding-android-arm64": ["@yuku-codegen/binding-android-arm64@0.8.3", "", { "os": "android", "cpu": "arm64" }, "sha512-/EKnnqwvN7xYoVDhQEIEJTdPDwGW1wkFz/2Eku3ES/IJd4lcQh/OaIDFBmoJKvpe12enrb1TIoYh1fxasGXolA=="], - "@wallet-standard/features": ["@wallet-standard/features@1.1.1", "", { "dependencies": { "@wallet-standard/base": "^1.1.1" } }, "sha512-aCWYmVeSCGViyEU5k7GMoW8zxE4Gs+C1s1Pp2XLesvSNlnZ4PMES9HUnTB3hl0b3RVj7C61yze3IWyrncqg4MA=="], + "@yuku-codegen/binding-darwin-arm64": ["@yuku-codegen/binding-darwin-arm64@0.8.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DFAOliF5YIPv3ayNHGOJhIun6Af4kMaL/YXxf8ZtD1qrOIMFnX/AQBhwfvLalhwmmxuGA8AUteaKRHBvdKZFVA=="], - "@wallet-standard/wallet": ["@wallet-standard/wallet@1.1.1", "", { "dependencies": { "@wallet-standard/base": "^1.1.1" } }, "sha512-8WiRPaKk/wNNRZhB2eVhpR/JW7/aqTCMoZhgVUCujuzDmxxmGvsosMxdCG4NAdYkoyozAHCX8/xLtlWUn5mNdQ=="], - - "@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="], - - "@xstate/fsm": ["@xstate/fsm@1.6.5", "", {}, "sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw=="], - - "@xyflow/react": ["@xyflow/react@12.11.2", "", { "dependencies": { "@xyflow/system": "0.0.79", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "@types/react": ">=17", "@types/react-dom": ">=17", "react": ">=17", "react-dom": ">=17" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-eLAlDWJfWnQEhJwGMjlWdAXO9eYllKpliUmPQlAmOLxz6mExXuzMVDUKLMquixgkrtmMFFtug3jGKmYYld12cA=="], - - "@xyflow/system": ["@xyflow/system@0.0.79", "", { "dependencies": { "@types/d3-drag": "^3.0.7", "@types/d3-interpolate": "^3.0.4", "@types/d3-selection": "^3.0.10", "@types/d3-transition": "^3.0.8", "@types/d3-zoom": "^3.0.8", "d3-drag": "^3.0.0", "d3-interpolate": "^3.0.1", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0" } }, "sha512-czLyOh91NF0hIzbNzwi8I6GlqG23BHh2435OddfI6uiaLH3xdrdygO93gqgH1Bv9mhy8XPFQJOBn1FTq4LvEWA=="], - - "@yuku-codegen/binding-android-arm64": ["@yuku-codegen/binding-android-arm64@0.8.3", "", { "os": "android", "cpu": "arm64" }, "sha512-/EKnnqwvN7xYoVDhQEIEJTdPDwGW1wkFz/2Eku3ES/IJd4lcQh/OaIDFBmoJKvpe12enrb1TIoYh1fxasGXolA=="], - - "@yuku-codegen/binding-darwin-arm64": ["@yuku-codegen/binding-darwin-arm64@0.8.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DFAOliF5YIPv3ayNHGOJhIun6Af4kMaL/YXxf8ZtD1qrOIMFnX/AQBhwfvLalhwmmxuGA8AUteaKRHBvdKZFVA=="], - - "@yuku-codegen/binding-darwin-x64": ["@yuku-codegen/binding-darwin-x64@0.8.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-WlMh4/oEibaTzE9j5Zq8qnsrH4Ii4kWdcDv/Pj2Rb/MYSrKghtg+bxbWpPe/6zJD21p9zZBApQUxl8ECpZOJuQ=="], + "@yuku-codegen/binding-darwin-x64": ["@yuku-codegen/binding-darwin-x64@0.8.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-WlMh4/oEibaTzE9j5Zq8qnsrH4Ii4kWdcDv/Pj2Rb/MYSrKghtg+bxbWpPe/6zJD21p9zZBApQUxl8ECpZOJuQ=="], "@yuku-codegen/binding-freebsd-x64": ["@yuku-codegen/binding-freebsd-x64@0.8.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hoDOpPP0FTxPSD+6w0Gs4p8iL1yXe6jjIXcdzNxyT1KE6B3JI6O0gTIWQISJ+8QyNpNjIwBb7nHCdRavktJM6A=="], @@ -2811,14 +2278,6 @@ "@yuku-toolchain/types": ["@yuku-toolchain/types@0.8.3", "", {}, "sha512-9LN3HYs3A9qSPVFunsxlbfwBcUgexti3TmhOzIxB/UH8zFuaHQJXTRDcN17DW6cp1GsyZtiZA7f18uIra36Jag=="], - "@zxcvbn-ts/core": ["@zxcvbn-ts/core@3.0.4", "", { "dependencies": { "fastest-levenshtein": "1.0.16" } }, "sha512-aQeiT0F09FuJaAqNrxynlAwZ2mW/1MdXakKWNmGM1Qp/VaY6CnB/GfnMS2T8gB2231Esp1/maCWd8vTG4OuShw=="], - - "@zxcvbn-ts/language-common": ["@zxcvbn-ts/language-common@3.0.4", "", {}, "sha512-viSNNnRYtc7ULXzxrQIVUNwHAPSXRtoIwy/Tq4XQQdIknBzw4vz36lQLF6mvhMlTIlpjoN/Z1GFu/fwiAlUSsw=="], - - "abitype": ["abitype@1.3.0", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-fk6Te+bojIFrMvMZrnOO+SxCB+RUksTGOzq/60ZRvs1L+BVzvi2bqt9L3W/17ZLdZsyM1FuYf65P5nlmoiH1Bg=="], - - "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], - "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], @@ -2829,8 +2288,6 @@ "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], - "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], "ai": ["ai@6.0.239", "", { "dependencies": { "@ai-sdk/gateway": "3.0.161", "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.41", "@opentelemetry/api": "^1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-qTFRqxLxKVR0J8X1MAs0xR5nXNZbeQz5Nnhm6jeASQuADcYaZppcVE6/Fl0gKSARsT/6I5XOWWx+fhCdZl6BOw=="], @@ -2841,10 +2298,6 @@ "alchemy": ["alchemy@2.0.0-beta.72", "", { "dependencies": { "@alchemy.run/cloudflare-runtime": "2.0.0-beta.72", "@alchemy.run/node-utils": "0.0.5", "@aws-sdk/credential-providers": "^3.0.0", "@clack/prompts": "^1.7.0", "@distilled.cloud/aws": "1.0.0-rc.4", "@distilled.cloud/axiom": "1.0.0-rc.4", "@distilled.cloud/cloudflare": "1.0.0-rc.4", "@distilled.cloud/core": "1.0.0-rc.4", "@distilled.cloud/neon": "1.0.0-rc.4", "@distilled.cloud/planetscale": "1.0.0-rc.4", "@effect/sql-d1": ">=4.0.0-beta.105 || >=4.0.0", "@effect/sql-sqlite-do": ">=4.0.0-beta.105 || >=4.0.0", "@effect/vitest": ">=4.0.0-beta.105 || >=4.0.0", "@libsql/client": "^0.17.0", "@octokit/rest": "^22.0.1", "@octokit/webhooks": "^14.2.0", "@prisma/dev": "^0.20.0", "@smithy/node-config-provider": "^4.0.0", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "@types/aws-lambda": "^8.10.152", "aws4fetch": "^1.0.20", "capnweb": "^0.6.1", "fast-glob": "^3.3.2", "fast-xml-parser": "^5.3.4", "ink": "^6.3.1", "jszip": "^3.10.1", "libsodium-wrappers": "^0.8.3", "pathe": "^2.0.3", "picomatch": "^4.0.4", "react": "^19.2.0", "rolldown": "1.1.5", "undici": "^7.16.0", "yaml": "^2.0.0" }, "peerDependencies": { "@aws/durable-execution-sdk-js": "^2.1.0", "@effect/platform-bun": ">=4.0.0-beta.105 || >=4.0.0", "@effect/platform-node": ">=4.0.0-beta.105 || >=4.0.0", "@effect/sql-mysql2": ">=4.0.0-beta.105 || >=4.0.0", "@effect/sql-pg": ">=4.0.0-beta.105 || >=4.0.0", "@vercel/nft": "^1.10.2", "drizzle-kit": "1.0.0-rc.5-ab785fc", "drizzle-orm": "1.0.0-rc.5-ab785fc", "effect": ">=4.0.0-beta.105 || >=4.0.0", "mongodb": "^6.10.0", "mysql2": "^3.23.2", "pg": "^8.22.0", "vite": "^8.0.7", "ws": "^8.20.0" }, "optionalPeers": ["@aws/durable-execution-sdk-js", "@effect/platform-bun", "@effect/platform-node", "@effect/sql-mysql2", "@effect/sql-pg", "@vercel/nft", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "pg", "vite", "ws"], "bin": { "alchemy": "bin/cli.js" } }, "sha512-3nD1U4hWdGTqJp3eeEXumEYQL6WmoFL8blzWB0VnodM8JD8ginGr9vYsexsrpqyH78tU/r6EBLBkzbbKgAwIQg=="], - "alien-signals": ["alien-signals@2.0.6", "", {}, "sha512-P3TxJSe31bUHBiblg59oU1PpaWPtmxF9GhJ/cB7OkgJ0qN/ifFSKUI25/v8ZhsT+lIG6ac8DpTOplXxORX6F3Q=="], - - "anser": ["anser@1.4.10", "", {}, "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww=="], - "ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="], "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], @@ -2879,8 +2332,6 @@ "arrayiffy-if-string": ["arrayiffy-if-string@5.1.3", "", {}, "sha512-LQk6w4KAE/65Yr1v9/1Z6dMXNnWrU5TxtQm5nFBNbqzoimKReG1tfYgmIctzMiYW1KgnsGXL8F9G0vlH0r01Ww=="], - "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], "ast-kit": ["ast-kit@2.2.0", "", { "dependencies": { "@babel/parser": "^7.28.5", "pathe": "^2.0.3" } }, "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw=="], @@ -2917,12 +2368,6 @@ "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="], - "babel-jest": ["babel-jest@29.7.0", "", { "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" } }, "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg=="], - - "babel-plugin-istanbul": ["babel-plugin-istanbul@6.1.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-instrument": "^5.0.4", "test-exclude": "^6.0.0" } }, "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA=="], - - "babel-plugin-jest-hoist": ["babel-plugin-jest-hoist@29.6.3", "", { "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", "@types/babel__core": "^7.1.14", "@types/babel__traverse": "^7.0.6" } }, "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg=="], - "babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.17", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-define-polyfill-provider": "^0.6.8", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w=="], "babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.13.0", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5", "core-js-compat": "^3.43.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A=="], @@ -2931,22 +2376,8 @@ "babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], - "babel-plugin-react-native-web": ["babel-plugin-react-native-web@0.21.2", "", {}, "sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA=="], - - "babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.32.0", "", { "dependencies": { "hermes-parser": "0.32.0" } }, "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg=="], - - "babel-plugin-transform-flow-enums": ["babel-plugin-transform-flow-enums@0.0.2", "", { "dependencies": { "@babel/plugin-syntax-flow": "^7.12.1" } }, "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ=="], - - "babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.2.0", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg=="], - - "babel-preset-expo": ["babel-preset-expo@55.0.24", "", { "dependencies": { "@babel/generator": "^7.20.5", "@babel/helper-module-imports": "^7.25.9", "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-transform-class-static-block": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.25.9", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.83.10", "babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-native-web": "~0.21.0", "babel-plugin-syntax-hermes-parser": "^0.32.0", "babel-plugin-transform-flow-enums": "^0.0.2", "debug": "^4.3.4", "resolve-from": "^5.0.0" }, "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", "expo-widgets": "^55.0.20", "react-refresh": ">=0.14.0 <1.0.0" }, "optionalPeers": ["@babel/runtime", "expo", "expo-widgets"] }, "sha512-dzjg70cq3Ls5msL79GSktjqtbjzXh/r5errxz8aD97S180pkXrMga22ozrcNhHddMBtZGDKN2jBK1FS1RLkfBQ=="], - - "babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="], - "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "bare-events": ["bare-events@2.9.1", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg=="], "bare-fs": ["bare-fs@4.8.0", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q=="], @@ -2959,62 +2390,36 @@ "base-64": ["base-64@1.0.0", "", {}, "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="], - "base-x": ["base-x@3.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA=="], - "base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="], - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.11.9", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-cp447VUsGS07+n1Dqf7YSQ8maeJrjEhaDxTm1ZefbqDtypHBC5GzGMQbklR6IPR13Y8OAJRHZWEMtZipJLCttg=="], "basic-ftp": ["basic-ftp@5.3.1", "", {}, "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw=="], "before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], - "better-opn": ["better-opn@3.0.2", "", { "dependencies": { "open": "^8.0.4" } }, "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ=="], - "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], - "big-integer": ["big-integer@1.6.52", "", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="], - "birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="], "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], - "bn.js": ["bn.js@5.2.5", "", {}, "sha512-Vq886eXykuP5E6HcKSSStP3bJgrE6In5WKxVUvJ8XGpWWYs2xZHWqUwzCtGgEtBcxyd57KBFDPFoUfNzdaHCNg=="], - "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], "boolbase": ["boolbase@2.0.0", "", {}, "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA=="], - "borsh": ["borsh@0.7.0", "", { "dependencies": { "bn.js": "^5.2.0", "bs58": "^4.0.0", "text-encoding-utf-8": "^1.0.2" } }, "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA=="], - "bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="], "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], "boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="], - "bplist-creator": ["bplist-creator@0.1.0", "", { "dependencies": { "stream-buffers": "2.2.x" } }, "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg=="], - - "bplist-parser": ["bplist-parser@0.3.2", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ=="], - - "brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "browser-tabs-lock": ["browser-tabs-lock@1.3.0", "", { "dependencies": { "lodash": ">=4.17.21" } }, "sha512-g6nHaobTiT0eMZ7jh16YpD2kcjAp+PInbiVq3M1x6KKaEIVhT4v9oURNIpZLOZ3LQbQ3XYfNhMAb/9hzNLIWrw=="], - "browserslist": ["browserslist@4.28.7", "", { "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", "electron-to-chromium": "^1.5.393", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw=="], - "bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], - - "bser": ["bser@2.1.1", "", { "dependencies": { "node-int64": "^0.4.0" } }, "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ=="], - "btoa-lite": ["btoa-lite@1.0.0", "", {}, "sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA=="], - "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], @@ -3069,10 +2474,6 @@ "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - "chrome-launcher": ["chrome-launcher@0.15.2", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0" }, "bin": { "print-chrome-path": "bin/print-chrome-path.js" } }, "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ=="], - - "chromium-edge-launcher": ["chromium-edge-launcher@0.2.0", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0", "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg=="], - "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], @@ -3103,8 +2504,6 @@ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], - "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "cluster-key-slot": ["cluster-key-slot@1.1.1", "", {}, "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw=="], @@ -3115,16 +2514,12 @@ "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], - "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "color-shorthand-hex-to-six-digit": ["color-shorthand-hex-to-six-digit@5.1.3", "", { "dependencies": { "codsen-utils": "^1.7.3", "hex-color-regex": "^1.1.0", "rfdc": "^1.4.1" } }, "sha512-uHNVXSceG5/sJ5abbk9i6ZuzYVIRI2MwgdcX13rFHD7oxiGq8bJOkOiJI7Pbjrl0zIm13hfEGefGoiQaLTi3XQ=="], - "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], - "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], @@ -3135,18 +2530,10 @@ "common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="], - "compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="], - - "compression": ["compression@1.8.1", "", { "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" } }, "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w=="], - - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - "conf": ["conf@13.1.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^9.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.6.3", "uint8array-extras": "^1.4.0" } }, "sha512-Bi6v586cy1CoTFViVO4lGTtx780lfF96fUmS1lSX6wpZf6330NvHUu6fReVuDP1de8Mg0nkZb01c8tAQdz1o3w=="], "confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="], - "connect": ["connect@3.7.0", "", { "dependencies": { "debug": "2.6.9", "finalhandler": "1.1.2", "parseurl": "~1.3.3", "utils-merge": "1.0.1" } }, "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ=="], - "consola": ["consola@3.2.3", "", {}, "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ=="], "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], @@ -3163,8 +2550,6 @@ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - "core-js": ["core-js@3.47.0", "", {}, "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg=="], - "core-js-compat": ["core-js-compat@3.49.0", "", { "dependencies": { "browserslist": "^4.28.1" } }, "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA=="], "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], @@ -3177,8 +2562,6 @@ "crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], - "crypto-js": ["crypto-js@4.2.0", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="], - "crypto-random-string": ["crypto-random-string@4.0.0", "", { "dependencies": { "type-fest": "^1.0.1" } }, "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA=="], "css-select": ["css-select@7.0.0", "", { "dependencies": { "boolbase": "^2.0.0", "css-what": "^8.0.0", "domhandler": "^6.0.1", "domutils": "^4.0.2", "nth-check": "^3.0.1" } }, "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g=="], @@ -3279,8 +2662,6 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], - "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], @@ -3291,16 +2672,12 @@ "dedent": ["dedent@1.5.1", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg=="], - "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], - "deepmerge-ts": ["deepmerge-ts@5.1.0", "", {}, "sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw=="], "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], - "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], - "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], "define-property": ["define-property@1.0.0", "", { "dependencies": { "is-descriptor": "^1.0.0" } }, "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA=="], @@ -3311,8 +2688,6 @@ "delaunator": ["delaunator@5.1.0", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="], - "delay": ["delay@5.0.0", "", {}, "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw=="], - "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], @@ -3325,12 +2700,8 @@ "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], - "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], - "deterministic-object-hash": ["deterministic-object-hash@2.0.2", "", { "dependencies": { "base-64": "^1.0.0" } }, "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ=="], "devalue": ["devalue@5.9.0", "", {}, "sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A=="], @@ -3339,12 +2710,8 @@ "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], - "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], - "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], - "dnssd-advertise": ["dnssd-advertise@1.1.6", "", {}, "sha512-Ndrrf6BMPalkQPd/zubL+4YghH2J9NspapQ09uDXwYbvOPkP0oaqf5CkcwJ0b50kS2O3ul6yVu+jz+RY62Cejg=="], - "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], "dom-serializer": ["dom-serializer@3.1.1", "", { "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", "entities": "^8.0.0" } }, "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw=="], @@ -3401,8 +2768,6 @@ "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], - "error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="], - "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], "ervy": ["ervy@1.0.7", "", {}, "sha512-LyHLPwIKxCKKtTO/qBMFzwA1BD5IjpM0AA3k6CeK9hrEn4Kbayi93G9eD/Ko4suEIjerSl7YpMmaOoL0g9OCoQ=="], @@ -3419,10 +2784,6 @@ "es-toolkit": ["es-toolkit@1.50.0", "", {}, "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w=="], - "es6-promise": ["es6-promise@4.2.8", "", {}, "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="], - - "es6-promisify": ["es6-promisify@5.0.0", "", { "dependencies": { "es6-promise": "^4.0.3" } }, "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ=="], - "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], @@ -3435,7 +2796,7 @@ "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + "escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], @@ -3461,8 +2822,6 @@ "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], - "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="], @@ -3473,64 +2832,6 @@ "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], - "expo": ["expo@55.0.28", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "55.0.34", "@expo/config": "~55.0.19", "@expo/config-plugins": "~55.0.11", "@expo/devtools": "55.0.3", "@expo/fingerprint": "0.16.8", "@expo/local-build-cache-provider": "55.0.15", "@expo/log-box": "55.0.13", "@expo/metro": "~55.1.1", "@expo/metro-config": "55.0.25", "@expo/vector-icons": "^15.0.2", "@ungap/structured-clone": "^1.3.0", "babel-preset-expo": "~55.0.24", "expo-asset": "~55.0.18", "expo-constants": "~55.0.17", "expo-file-system": "~55.0.24", "expo-font": "~55.0.8", "expo-keep-awake": "~55.0.8", "expo-modules-autolinking": "55.0.25", "expo-modules-core": "55.0.25", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-minimum": "^0.1.2" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli", "fingerprint": "bin/fingerprint", "expo-modules-autolinking": "bin/autolinking" } }, "sha512-rKWG+gjM4e+nJ6UUXn+jhs1+2lkzwLHjUZL9U4ejN3mLtqU9sOYvX925tOaD/lu3847LoZ8i5GJiyqu3tmK7Lg=="], - - "expo-application": ["expo-application@55.0.17", "", { "peerDependencies": { "expo": "*" } }, "sha512-ASuZe+Sl8ax+0pQOjd8L5cz5xpxW0F1VSorRKFr9LHxDSQwH929nYjbMrvW6KlRIIWZBIAQQsYqWiowytCLMRA=="], - - "expo-asset": ["expo-asset@55.0.18", "", { "dependencies": { "@expo/image-utils": "^0.8.15", "expo-constants": "~55.0.17" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-dbNMcBE0AaFbdxjBSRgDLdtOi8PHt9CQOg+J3EdeXNPKZCDO88kvaHcXaYUMmkbN2KJYQlfu1GxfUVAIoi7iVQ=="], - - "expo-auth-session": ["expo-auth-session@55.0.17", "", { "dependencies": { "expo-application": "~55.0.16", "expo-constants": "~55.0.16", "expo-crypto": "~55.0.16", "expo-linking": "~55.0.16", "expo-web-browser": "~55.0.17", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-+V+hc0wDd8Fg+1jnvG5FZ5Ngsl45YyDW0W5CVPzYQfdIHuOu73y7fycbvKQfg0NEq2p6Acl5nWdqgeG7ZeZtHg=="], - - "expo-constants": ["expo-constants@55.0.17", "", { "dependencies": { "@expo/env": "~2.1.3" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-t0SNWmXTjXPCHzjNsv1Okjaj8SpXQcUjDPtYDqvofDS+BhAsvlKNmwaaWXvduBjjmmmJWNH8q1/x9IWQ+upg8g=="], - - "expo-crypto": ["expo-crypto@55.0.17", "", { "peerDependencies": { "expo": "*" } }, "sha512-MWe1IsAzPzKGfTqoOKDef5vFvTj2uke6FzcrtbNsy5I9Kvj3JH+DNxEDcVO/wc9gMO5AEUMWK/LC0J5+NkQsEw=="], - - "expo-dev-client": ["expo-dev-client@55.0.37", "", { "dependencies": { "expo-dev-launcher": "55.0.38", "expo-dev-menu": "55.0.32", "expo-dev-menu-interface": "55.0.2", "expo-manifests": "~55.0.19", "expo-updates-interface": "~55.1.6" }, "peerDependencies": { "expo": "*" } }, "sha512-FA5dMuo653p02fAG4gWRSRaERP3jXIfpVngAE2F9pZLe0wHHJAbIlNWrr+TdaIRQ0xX02FTyLkXOgxvxSqku2w=="], - - "expo-dev-launcher": ["expo-dev-launcher@55.0.38", "", { "dependencies": { "@expo/schema-utils": "^55.0.5", "expo-dev-menu": "55.0.32", "expo-manifests": "~55.0.19" }, "peerDependencies": { "expo": "*" } }, "sha512-i7KX1b0YUcjpSHbr9xOpGF8FPr2+41ZQJPqtKEzuYtMgtCEDonUDpvSbEEE5RDxywxKqrV6hKtV058ncv0gAbw=="], - - "expo-dev-menu": ["expo-dev-menu@55.0.32", "", { "dependencies": { "expo-dev-menu-interface": "55.0.2" }, "peerDependencies": { "expo": "*" } }, "sha512-4PlXKz9iDVqGpkRKPKxkDQdzg/snpRqPIHagsnGx1h29fxXwmeuZzC/Bju8aZkMcAXCNdgCDHjyjp7Ff5jCbdg=="], - - "expo-dev-menu-interface": ["expo-dev-menu-interface@55.0.2", "", { "peerDependencies": { "expo": "*" } }, "sha512-DomUNvGzY/xliwnMdbAYY780sCv19N7zIbifc0ClcoCzJZpNSCkvJ2qGIFRPyM/7DmqmlHGCKi8di7kYYLKNEg=="], - - "expo-file-system": ["expo-file-system@55.0.24", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-7/HJdvaaf3kP5T3atd+6Q0/QexrGio4Fs2GVtp7G8d/xQCSu01yeGReu7tGQo9VAM67Snq+ujGlL8q2wbMXLkQ=="], - - "expo-font": ["expo-font@55.0.8", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-WyP75pnKqhLNktYwDn3xKAUNt5rLihRDv8XWGhhz6VEhVqypixpT86NA3uGtiDTlM3gGjhrYCY7o7ypXgCUOZg=="], - - "expo-glass-effect": ["expo-glass-effect@55.0.11", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-wqq7GUOqSkfoFJzreZvBG0jzjsq5c582m3glhWSjcmIuByxXXWp6j6GY6hyFuYKzpOXhbuvusVxGCQi0yWnp3g=="], - - "expo-haptics": ["expo-haptics@55.0.16", "", { "peerDependencies": { "expo": "*" } }, "sha512-dplF3i1DQPTZWMrqGbBzDFrd7cxpWnaZE0Ahimyc07V6nFsx939SAeX6jK3XYK/xYn+ZuV05ggBA18PKbHl2YA=="], - - "expo-image": ["expo-image@55.0.11", "", { "dependencies": { "sf-symbols-typescript": "^2.2.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-PVIBYQJW/h1f6Zb9xnoWlgfqyOPVm2yb6eo6ZogaKbvMrhb/Q/fiERbagi4oqmR6IPljWPEpkXXQyFBUh7TjpQ=="], - - "expo-json-utils": ["expo-json-utils@55.0.2", "", {}, "sha512-QJMOZOPOG7CTnKcrdVaiummn2va1MCO56z++eyWkDv3GBRODldM6MFMDf/jTREWthFc2Nxo6TuyWRrEV9S6n/Q=="], - - "expo-keep-awake": ["expo-keep-awake@55.0.8", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-PfIpMfM+STOBwkR5XOE+yVtER86c44MD+W8QD8JxuO0sT9pF7Y1SJYakWlpvX8xsGA+bjKLxftm9403s9kQhKA=="], - - "expo-linking": ["expo-linking@55.0.16", "", { "dependencies": { "expo-constants": "~55.0.16", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-O+Idexholc5rlr6Z4W/+TewTLQVnbEXztoLgLEbJwh5/A1SsJ+eq9yGGPwd4rcQ+fEBDiExr6qZxbUArnUGHfw=="], - - "expo-manifests": ["expo-manifests@55.0.19", "", { "dependencies": { "expo-json-utils": "~55.0.2" }, "peerDependencies": { "expo": "*" } }, "sha512-NmIoGapFzTTZhR46loLqTTnZfL8C6GQdQA47q1NjWiT3UeUPrGUrK7CEASMdhCFxaymkpbRFsbMECeSq6knIPg=="], - - "expo-modules-autolinking": ["expo-modules-autolinking@55.0.25", "", { "dependencies": { "@expo/require-utils": "^55.0.6", "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-qO8se6zTxfdCGeuwc0dCTmnstW5r1tKSr9SoWtZn0mhlr5Qe5RSJa0vhzQ32eIwgMd77GJrc1yJtkktgN817vg=="], - - "expo-modules-core": ["expo-modules-core@55.0.25", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-worklets": "^0.7.4 || ^0.8.0" }, "optionalPeers": ["react-native-worklets"] }, "sha512-yXpfg7aHLbuqoXocK34Vua6Aey5SCyqLygAsXAMbul9P8vfBjLpaOPiTJ5cLVF7Drfq8ownqVJO6qpGEtZ6GOw=="], - - "expo-router": ["expo-router@55.0.17", "", { "dependencies": { "@expo/metro-runtime": "^55.0.12", "@expo/schema-utils": "^55.0.5", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.12", "@react-navigation/bottom-tabs": "^7.15.5", "@react-navigation/native": "^7.1.33", "@react-navigation/native-stack": "^7.14.5", "client-only": "^0.0.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "expo-glass-effect": "^55.0.11", "expo-image": "^55.0.11", "expo-server": "^55.0.11", "expo-symbols": "^55.0.9", "fast-deep-equal": "^3.1.3", "invariant": "^2.2.4", "nanoid": "^3.3.8", "query-string": "^7.1.3", "react-fast-compare": "^3.2.2", "react-native-is-edge-to-edge": "^1.2.1", "semver": "~7.6.3", "server-only": "^0.0.1", "sf-symbols-typescript": "^2.1.0", "shallowequal": "^1.1.0", "use-latest-callback": "^0.2.1", "vaul": "^1.1.2" }, "peerDependencies": { "@expo/log-box": "55.0.13", "@react-navigation/drawer": "^7.9.4", "@testing-library/react-native": ">= 13.2.0", "expo": "*", "expo-constants": "^55.0.17", "expo-linking": "^55.0.16", "react": "*", "react-dom": "*", "react-native": "*", "react-native-gesture-handler": "*", "react-native-reanimated": "*", "react-native-safe-area-context": ">= 5.4.0", "react-native-screens": "*", "react-native-web": "*", "react-server-dom-webpack": "~19.0.4 || ~19.1.5 || ~19.2.4" }, "optionalPeers": ["@react-navigation/drawer", "@testing-library/react-native", "react-dom", "react-native-gesture-handler", "react-native-reanimated", "react-native-web", "react-server-dom-webpack"] }, "sha512-LcNZTXd9S0ifq2lQ3RfOn+JzKcA3KYvCrkTC/oa8rBYuFl8wBPf3L/rlVVURY57JZN+F2XZtTvY3/l56atjzbg=="], - - "expo-secure-store": ["expo-secure-store@55.0.16", "", { "peerDependencies": { "expo": "*" } }, "sha512-4DkdluBLJnqndKMpvg4bTVoyi1D5p3V7nxyYvlzLvuqMVrUzXPTGTyIw347H3wVU1jp+sfGNX5xqtpQCY3hF+A=="], - - "expo-server": ["expo-server@55.0.11", "", {}, "sha512-AxRdHqcv0H1g4s923vu+5n1Nrhne23bjXbP+Vl7+Lwfpe7MG9PuU1IS95IJK6a+7BVV1mRN6QlZvs8Yv7EEXNQ=="], - - "expo-splash-screen": ["expo-splash-screen@55.0.23", "", { "dependencies": { "@expo/prebuild-config": "^55.0.20" }, "peerDependencies": { "expo": "*" } }, "sha512-gvxl1bWvcxWwr8Hd5hMjCkOYIgK+IY0tEFSLlhvVJIKr2vMUkz1RYMgC4732lhn4PZXvwjeFUsREvjzGMRsuqA=="], - - "expo-symbols": ["expo-symbols@55.0.9", "", { "dependencies": { "@expo-google-fonts/material-symbols": "^0.4.1", "sf-symbols-typescript": "^2.0.0" }, "peerDependencies": { "expo": "*", "expo-font": "*", "react": "*", "react-native": "*" } }, "sha512-F85C/8ExQjd2gYjasLVKMT8wPj+1+19TVTqg4jAeVjVZklqiQtLO72io9Ji1xAjYNgmDeUI0diVHlFMMTC4Ekg=="], - - "expo-updates-interface": ["expo-updates-interface@55.1.6", "", { "peerDependencies": { "expo": "*" } }, "sha512-evxNpagCkjT3lE6bGV570TFzRtKuIuLY8I37RYHoriXCJ+ZKCN1hbmklK29uAixya+BxGpeTI2K4FqYeJLvfrw=="], - - "expo-web-browser": ["expo-web-browser@55.0.18", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-7b6N47OZ/TEArHAoSeq86pWzZvqdITlf9Zd+e520bKUXVRd1WxkyskZekJbcwusBpyhSqgWRupITZeF+adlddQ=="], - - "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], - "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "exsolve": ["exsolve@1.1.1", "", {}, "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g=="], @@ -3541,8 +2842,6 @@ "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], - "eyes": ["eyes@0.1.8", "", {}, "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ=="], - "fast-check": ["fast-check@4.9.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -3553,12 +2852,8 @@ "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - "fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="], - "fast-stable-stringify": ["fast-stable-stringify@1.0.0", "", {}, "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag=="], - "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], @@ -3571,22 +2866,14 @@ "fast-xml-parser": ["fast-xml-parser@5.10.1", "", { "dependencies": { "@nodable/entities": "^3.0.0", "fast-xml-builder": "^1.2.0", "is-unsafe": "^2.0.0", "path-expression-matcher": "^1.6.2", "strnum": "^2.4.1", "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw=="], - "fastest-levenshtein": ["fastest-levenshtein@1.0.16", "", {}, "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg=="], - "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], - "fb-dotslash": ["fb-dotslash@0.5.8", "", { "bin": { "dotslash": "bin/dotslash" } }, "sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA=="], - - "fb-watchman": ["fb-watchman@2.0.2", "", { "dependencies": { "bser": "2.1.1" } }, "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA=="], - "fd-package-json": ["fd-package-json@2.0.0", "", { "dependencies": { "walk-up-path": "^4.0.0" } }, "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ=="], "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - "fetch-nodeshim": ["fetch-nodeshim@0.4.10", "", {}, "sha512-m6I8ALe4L4XpdETy7MJZWs6L1IVMbjs99bwbpIKphxX+0CTns4IKDWJY0LWfr4YsFjfg+z1TjzTMU8lKl8rG0w=="], - "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], @@ -3595,18 +2882,12 @@ "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], - "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], - "flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="], - "flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], - "follow-redirects": ["follow-redirects@1.16.0", "", { "peerDependencies": { "debug": "*" }, "optionalPeers": ["debug"] }, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], "fontace": ["fontace@0.4.1", "", { "dependencies": { "fontkitten": "^1.0.2" } }, "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw=="], - "fontfaceobserver": ["fontfaceobserver@2.3.0", "", {}, "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg=="], - "fontkitten": ["fontkitten@1.0.3", "", { "dependencies": { "tiny-inflate": "^1.0.3" } }, "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw=="], "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], @@ -3623,8 +2904,6 @@ "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -3641,10 +2920,6 @@ "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], - - "get-package-type": ["get-package-type@0.1.0", "", {}, "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q=="], - "get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], @@ -3655,12 +2930,8 @@ "get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="], - "getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="], - "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], @@ -3723,22 +2994,12 @@ "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], - "hermes-compiler": ["hermes-compiler@0.14.1", "", {}, "sha512-+RPPQlayoZ9n6/KXKt5SFILWXCGJ/LV5d24L5smXrvTDrPS4L6dSctPczXauuvzFP3QEJbD1YO7Z3Ra4a+4IhA=="], - - "hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="], - - "hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="], - "hex-color-regex": ["hex-color-regex@1.1.0", "", {}, "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ=="], - "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], - "hono": ["hono@4.12.33", "", {}, "sha512-+SwvkaiJtxsiPjhy9LivY/1m7UsNqCJetM1BrZl9A5DkQhlbHQDU730mMiDPWjnoCYOM8Chf3WrCJw27kNTPFQ=="], "hookable": ["hookable@6.1.1", "", {}, "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ=="], - "hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], - "html-crush": ["html-crush@6.1.3", "", { "dependencies": { "codsen-utils": "^1.7.3", "ranges-apply": "^7.1.3", "ranges-push": "^7.1.3", "string-left-right": "^6.1.3", "string-match-left-right": "^9.1.3", "string-range-expander": "^4.1.3", "test-mixer": "^4.2.3" } }, "sha512-IrDC4BrdrMmV+GMYfXtx6BtzQ6hVf+GjLSDhmg/14DkpNbXtxAcbH3Wk9VeNZbg/y2MpobWTVOSrCzr/HWBwcw=="], "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], @@ -3765,17 +3026,11 @@ "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], - "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], - "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], "idb-keyval": ["idb-keyval@6.2.1", "", {}, "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg=="], - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "image-size": ["image-size@1.2.1", "", { "dependencies": { "queue": "6.0.2" }, "bin": { "image-size": "bin/image-size.js" } }, "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw=="], + "ignore": ["ignore@5.3.1", "", {}, "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw=="], "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], @@ -3787,12 +3042,8 @@ "import-without-cache": ["import-without-cache@0.4.0", "", {}, "sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ=="], - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], - "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ink": ["ink@6.8.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.4", "ansi-escapes": "^7.3.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^5.1.1", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "scheduler": "^0.27.0", "signal-exit": "^3.0.7", "slice-ansi": "^8.0.0", "stack-utils": "^2.0.6", "string-width": "^8.1.1", "terminal-size": "^4.0.1", "type-fest": "^5.4.1", "widest-line": "^6.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": ">=6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA=="], @@ -3821,8 +3072,6 @@ "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], - "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="], - "ioredis": ["ioredis@5.11.1", "", { "dependencies": { "@ioredis/commands": "1.10.0", "cluster-key-slot": "1.1.1", "debug": "4.4.3", "denque": "2.1.0", "redis-errors": "1.2.0", "redis-parser": "3.0.0", "standard-as-callback": "2.1.0" } }, "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A=="], "ip-address": ["ip-address@10.4.0", "", {}, "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ=="], @@ -3837,8 +3086,6 @@ "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], - "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], - "is-buffer": ["is-buffer@1.1.6", "", {}, "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="], "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], @@ -3897,36 +3144,6 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "isomorphic-ws": ["isomorphic-ws@4.0.1", "", { "peerDependencies": { "ws": "*" } }, "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w=="], - - "isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="], - - "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], - - "istanbul-lib-instrument": ["istanbul-lib-instrument@5.2.1", "", { "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" } }, "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg=="], - - "jayson": ["jayson@4.3.0", "", { "dependencies": { "@types/connect": "^3.4.33", "@types/node": "^12.12.54", "@types/ws": "^7.4.4", "commander": "^2.20.3", "delay": "^5.0.0", "es6-promisify": "^5.0.0", "eyes": "^0.1.8", "isomorphic-ws": "^4.0.1", "json-stringify-safe": "^5.0.1", "stream-json": "^1.9.1", "uuid": "^8.3.2", "ws": "^7.5.10" }, "bin": { "jayson": "bin/jayson.js" } }, "sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ=="], - - "jest-environment-node": ["jest-environment-node@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw=="], - - "jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="], - - "jest-haste-map": ["jest-haste-map@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "walker": "^1.0.8" }, "optionalDependencies": { "fsevents": "^2.3.2" } }, "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA=="], - - "jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], - - "jest-mock": ["jest-mock@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" } }, "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw=="], - - "jest-regex-util": ["jest-regex-util@29.6.3", "", {}, "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg=="], - - "jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], - - "jest-validate": ["jest-validate@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", "pretty-format": "^29.7.0" } }, "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw=="], - - "jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], - - "jimp-compact": ["jimp-compact@0.16.1", "", {}, "sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww=="], - "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], "js-base64": ["js-base64@3.9.2", "", {}, "sha512-6zayE8QlUdiweYI6cETD/XBSqFcoCUlufn/29PJR99r82x1yDnIprRca0YvAYpAW+ez0GuQkVBC6xG5QkD7OjA=="], @@ -3937,8 +3154,6 @@ "js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="], - "jsc-safe-url": ["jsc-safe-url@0.2.4", "", {}, "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q=="], - "jsdom": ["jsdom@29.1.1", "", { "dependencies": { "@asamuzakjp/css-color": "^5.1.11", "@asamuzakjp/dom-selector": "^7.1.1", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.3", "@exodus/bytes": "^1.15.0", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.3.5", "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", "undici": "^7.25.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], @@ -3949,8 +3164,6 @@ "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], - "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], - "json-with-bigint": ["json-with-bigint@3.5.10", "", {}, "sha512-Vcx+JVNEBts/xfcoCS69sKrOhOk/3TVlvlT+XzUOefVKnnrbYSCKpDCm10pohsJFtsJVYnwa/cXRZ4eElzaM6w=="], "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], @@ -3979,14 +3192,10 @@ "kysely": ["kysely@0.28.17", "", {}, "sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q=="], - "lan-network": ["lan-network@0.2.1", "", { "bin": { "lan-network": "dist/lan-network-cli.js" } }, "sha512-ONPnazC96VKDntab9j9JKwIWhZ4ZUceB4A9Epu4Ssg0hYFmtHZSeQ+n15nIwTFmcBUKtExOer8WTJ4GF9MO64A=="], - "launch-editor": ["launch-editor@2.14.1", "", { "dependencies": { "picocolors": "^1.1.1", "shell-quote": "^1.8.4" } }, "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA=="], "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], - "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], - "libsodium": ["libsodium@0.8.4", "", {}, "sha512-lMcYaRi0zcs7tarATsQUYC7rstliIXZuoq0c6zXSgNtSNtdvBgkSegjWhpMJAXzKX3SUSwIp7+zEsob+j3LuRw=="], "libsodium-wrappers": ["libsodium-wrappers@0.8.4", "", { "dependencies": { "libsodium": "^0.8.0" } }, "sha512-mu8aAWucZjTB5O/BtGXtW4e1agy7uHxNYG7zPthmmD1jU43LCDmSWZLN4JhflbdPXj3yDO4lxM1O9hLDgIOXDw=="], @@ -3995,8 +3204,6 @@ "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], - "lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="], - "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], @@ -4027,9 +3234,7 @@ "local-pkg": ["local-pkg@1.2.1", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q=="], - "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - - "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], @@ -4049,8 +3254,6 @@ "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], - "lodash.throttle": ["lodash.throttle@4.1.1", "", {}, "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="], - "log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], @@ -4073,8 +3276,6 @@ "magicast": ["magicast@0.5.4", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "source-map-js": "^1.2.1" } }, "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w=="], - "makeerror": ["makeerror@1.0.12", "", { "dependencies": { "tmpl": "1.0.5" } }, "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg=="], - "markdown-exit": ["markdown-exit@1.1.0-beta.2", "", { "dependencies": { "@types/linkify-it": "^5.0.0", "@types/mdurl": "^2.0.0", "entities": "^7.0.0", "linkify-it": "^5.0.1", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" } }, "sha512-8CzMGVlFZ4DEfnc8KU+4ycUW2SIOuiXqCHD7z51ecVEi/weyc0f2ylQbCm4KoKuVlTZSuMUMnWT0hTyquZ7anQ=="], "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], @@ -4085,8 +3286,6 @@ "marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="], - "marky": ["marky@1.3.0", "", {}, "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="], - "matcher": ["matcher@6.0.0", "", { "dependencies": { "escape-string-regexp": "^5.0.0" } }, "sha512-TzDerdcNtI79w7Av4GT57bLdElPA/VAkjqdMZv8yhuc8geU2z0ljW9anXbX/55aHEMTpYypZb1lxsA/46r9oOQ=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -4137,46 +3336,14 @@ "media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], - "memoize-one": ["memoize-one@5.2.1", "", {}, "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="], - "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - "merge-options": ["merge-options@3.0.4", "", { "dependencies": { "is-plain-obj": "^2.1.0" } }, "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ=="], - "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "mermaid": ["mermaid@11.16.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.2.0", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.20", "dompurify": "^3.3.3", "es-toolkit": "^1.45.1", "katex": "^0.16.45", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA=="], - "metro": ["metro@0.83.7", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "accepts": "^2.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.35.0", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.83.7", "metro-cache": "0.83.7", "metro-cache-key": "0.83.7", "metro-config": "0.83.7", "metro-core": "0.83.7", "metro-file-map": "0.83.7", "metro-resolver": "0.83.7", "metro-runtime": "0.83.7", "metro-source-map": "0.83.7", "metro-symbolicate": "0.83.7", "metro-transform-plugins": "0.83.7", "metro-transform-worker": "0.83.7", "mime-types": "^3.0.1", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-SPaPEyvTsTmd0LpT7RaZciQyDw2i/JB7+iY9L5VfBo72+psescFxBqpI1TL9dnL+pmnfkU+l/J1mEEGLeF65EQ=="], - - "metro-babel-transformer": ["metro-babel-transformer@0.83.7", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.35.0", "metro-cache-key": "0.83.7", "nullthrows": "^1.1.1" } }, "sha512-sBqBkt6kNut/88bv+Ucvm4yqdPetbvAEsHzi3MAgJEifOSYYzX5Z5Kgw3TFOrwf/mHJTOBG2ONlaMHoyfP15TA=="], - - "metro-cache": ["metro-cache@0.83.7", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.7" } }, "sha512-E9SRePXQ1Zvlj79VcOk57q7VC7rMHMFQ+jhmPHBiq+dJ0bJB5BL87lWZF6oh5X76Cci5tpDuQNaDwwuSCToEeg=="], - - "metro-cache-key": ["metro-cache-key@0.83.7", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-W1c2Nmx8MiJTJt+eWhMO08z9VKi3kZOaz99IYGdqeqDgY9j+yZjXl62rUav4Di0heZfh4/n2s722PqRL1OODeg=="], - - "metro-config": ["metro-config@0.83.7", "", { "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.83.7", "metro-cache": "0.83.7", "metro-core": "0.83.7", "metro-runtime": "0.83.7", "yaml": "^2.6.1" } }, "sha512-83mjWFbFOt2GeJ6pFIum5mSnc1uTsZJAtD8o4ej0s4NVsYsA7fB+pHvTfHhFrpeMONaobu2riKavkPei05Er/Q=="], - - "metro-core": ["metro-core@0.83.7", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.7" } }, "sha512-6yn3w1wnltT6RQl7p7YES2l95ArC+mWrOssEiH8p5/DDrJS65/szf9LsC9JrBv8c5DdvSY3V3f0GRYg0Ox7hCg=="], - - "metro-file-map": ["metro-file-map@0.83.7", "", { "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-+j0F1m+FQYVAQ6syf+mwhIPV5GoFQrkInX8bppuc50IzNsZbMrp8R5H/Sx/K2daQ3YEa9F/XwkeZT8gzJfgeCw=="], - - "metro-minify-terser": ["metro-minify-terser@0.83.7", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-MfJar2IS4tBRuLb9svwb0Gu5l9BsH+pcRm8eGcEi/wy8MzZinfinh5dFLt2nWkocnulIgtGB5NkFDdbXqMXKhQ=="], - - "metro-resolver": ["metro-resolver@0.83.7", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-WSJIENlMcoSsuz66IfBHOkgfp3KJt2UW2TnEHPf1b8pIG2eEXNOVmo2+03A0H17WY2XGXWgxL0CG7FAopqgB1A=="], - - "metro-runtime": ["metro-runtime@0.83.7", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-9GKkJURaB2iyYoEExKnedzAHzxmKtSi+k0tsZUvMoU27tBZJElchYt7JH/Ai/XzYAI9lCAaV7u5HZSI8J5Z+wQ=="], - - "metro-source-map": ["metro-source-map@0.83.7", "", { "dependencies": { "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.7", "nullthrows": "^1.1.1", "ob1": "0.83.7", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-JgA1h7oc1a1jydBe1GhVFsUoMYo3wLPk7oRA32rjlDsq+sP2JLt9x2p2lWbNSxTm/u8NV4VRid3hvEJgcX8tKw=="], - - "metro-symbolicate": ["metro-symbolicate@0.83.7", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.7", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-g4suyxw20WOHWI680c+Kq4wC/NF+Hx5pRH9afrMp+sMTxqLeKcPR1Xf4wMhsjlbvx7LbIREdke6q928jEjvJWw=="], - - "metro-transform-plugins": ["metro-transform-plugins@0.83.7", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-Ss0FpBiZDjX2kwhukMDl5sNdYK8T/06IPqxNE4H6PTlRlfs9q11cef13c/xESY/Pm4VCkp1yJUZO3kXzvMxQFA=="], - - "metro-transform-worker": ["metro-transform-worker@0.83.7", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "metro": "0.83.7", "metro-babel-transformer": "0.83.7", "metro-cache": "0.83.7", "metro-cache-key": "0.83.7", "metro-minify-terser": "0.83.7", "metro-source-map": "0.83.7", "metro-transform-plugins": "0.83.7", "nullthrows": "^1.1.1" } }, "sha512-UegCo7ygB2fT64mRK2nbAjQVJ1zSwIIHy8d96jJv2nKZFDaViYBiughEdu5HM/Ceq0WN3LZrZk3zhl9aoiLYFw=="], - "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], @@ -4269,18 +3436,10 @@ "miniflare": ["miniflare@5.20260730.0-alpha", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.2", "undici": "7.28.0", "workerd": "1.20260730.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" } }, "sha512-8/dspSXDshP6nSkCpjKO7BYc2qZoYSXm7iM+QxY7qJyJpAB3onnQSaiu0cvKJlfuMGwULl55hG69FJCcCMXU1Q=="], - "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], - - "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], - "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], - "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], - "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], - "mobile": ["mobile@workspace:apps/mobile"], - "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], "motion": ["motion@12.43.0", "", { "dependencies": { "framer-motion": "^12.43.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-BQgQbSa9Hn3/mtbib0MK53y6JSANa+YKUKlaYnWzAVDH424RYQ5LVpV3pNiWH00BA2z4ojsSdMzqT7g2FQwjuQ=="], @@ -4299,8 +3458,6 @@ "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], - "multitars": ["multitars@1.0.0", "", {}, "sha512-H/J4fMLedtudftaYMOg7ajzLYgT3/rwbWVJbqr/iUgB8DQztn38ys5HOqI1CzSxx8QhXXwOOnnBvd4v3jG5+Mg=="], - "murmurhash3js": ["murmurhash3js@3.0.1", "", {}, "sha512-KL8QYUaxq7kUbcl0Yto51rMcYt7E/4N4BG3/c96Iqw1PQrTRspu8Cpx4TZ4Nunib1d4bEkIH3gjCYlP2RLBdow=="], "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], @@ -4321,18 +3478,12 @@ "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], - "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], - "node-forge": ["node-forge@1.4.0", "", {}, "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ=="], - "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], - "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], - "node-mock-http": ["node-mock-http@1.0.5", "", {}, "sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw=="], "node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="], @@ -4343,16 +3494,10 @@ "nostics": ["nostics@1.2.0", "", {}, "sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg=="], - "npm-package-arg": ["npm-package-arg@11.0.3", "", { "dependencies": { "hosted-git-info": "^7.0.0", "proc-log": "^4.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^5.0.0" } }, "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw=="], - "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], "nth-check": ["nth-check@3.0.1", "", { "dependencies": { "boolbase": "^2.0.0" } }, "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ=="], - "nullthrows": ["nullthrows@1.1.1", "", {}, "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="], - - "ob1": ["ob1@0.83.7", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-9M5kpuOLyTPogMtZiQUIxdAZxl7Dxs6tVBbJErSumsqGMuhVSoUbkfeZ3XNPpLpwBBtqY5QDUzGwggLHX3slQg=="], - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-boolean-combinations": ["object-boolean-combinations@6.2.3", "", { "dependencies": { "codsen-utils": "^1.7.3", "rfdc": "^1.4.1" } }, "sha512-A2inWgy5Hy3+9prMyiKBUBYcTpbuZbKOMt+YKMD3ZAsli7TwKh6TPSZdK+kNF5hmJFhaVbjHwP/xsfjO2Z/GmQ=="], @@ -4371,8 +3516,6 @@ "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - "on-headers": ["on-headers@1.1.0", "", {}, "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A=="], - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], @@ -4387,8 +3530,6 @@ "otel-collector-maple-exporter": ["otel-collector-maple-exporter@workspace:packages/otel-collector-maple-exporter"], - "ox": ["ox@0.6.9", "", { "dependencies": { "@adraffy/ens-normalize": "^1.10.1", "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", "@scure/bip32": "^1.5.0", "@scure/bip39": "^1.4.0", "abitype": "^1.0.6", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug=="], - "oxc-parser": ["oxc-parser@0.142.0", "", { "dependencies": { "@oxc-project/types": "^0.142.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.142.0", "@oxc-parser/binding-android-arm64": "0.142.0", "@oxc-parser/binding-darwin-arm64": "0.142.0", "@oxc-parser/binding-darwin-x64": "0.142.0", "@oxc-parser/binding-freebsd-x64": "0.142.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.142.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.142.0", "@oxc-parser/binding-linux-arm64-gnu": "0.142.0", "@oxc-parser/binding-linux-arm64-musl": "0.142.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.142.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.142.0", "@oxc-parser/binding-linux-riscv64-musl": "0.142.0", "@oxc-parser/binding-linux-s390x-gnu": "0.142.0", "@oxc-parser/binding-linux-x64-gnu": "0.142.0", "@oxc-parser/binding-linux-x64-musl": "0.142.0", "@oxc-parser/binding-openharmony-arm64": "0.142.0", "@oxc-parser/binding-wasm32-wasi": "0.142.0", "@oxc-parser/binding-win32-arm64-msvc": "0.142.0", "@oxc-parser/binding-win32-ia32-msvc": "0.142.0", "@oxc-parser/binding-win32-x64-msvc": "0.142.0" } }, "sha512-kKR+jPiRJYJDexVoziIg/FVGvr1fT1FZSSJOk6tVoMKKSlsf1Cso+cgGCJkOEDWOP174vRntCPFKg+AS7InWvw=="], "oxc-resolver": ["oxc-resolver@11.24.2", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.24.2", "@oxc-resolver/binding-android-arm64": "11.24.2", "@oxc-resolver/binding-darwin-arm64": "11.24.2", "@oxc-resolver/binding-darwin-x64": "11.24.2", "@oxc-resolver/binding-freebsd-x64": "11.24.2", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.24.2", "@oxc-resolver/binding-linux-arm-musleabihf": "11.24.2", "@oxc-resolver/binding-linux-arm64-gnu": "11.24.2", "@oxc-resolver/binding-linux-arm64-musl": "11.24.2", "@oxc-resolver/binding-linux-ppc64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-musl": "11.24.2", "@oxc-resolver/binding-linux-s390x-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-musl": "11.24.2", "@oxc-resolver/binding-openharmony-arm64": "11.24.2", "@oxc-resolver/binding-wasm32-wasi": "11.24.2", "@oxc-resolver/binding-win32-arm64-msvc": "11.24.2", "@oxc-resolver/binding-win32-x64-msvc": "11.24.2" } }, "sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw=="], @@ -4403,14 +3544,10 @@ "p-limit": ["p-limit@6.2.0", "", { "dependencies": { "yocto-queue": "^1.1.1" } }, "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA=="], - "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - "p-queue": ["p-queue@8.1.1", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ=="], "p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="], - "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], - "pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="], "pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="], @@ -4425,8 +3562,6 @@ "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], - "parse-png": ["parse-png@2.1.0", "", { "dependencies": { "pngjs": "^3.3.0" } }, "sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ=="], - "parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="], "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="], @@ -4441,18 +3576,12 @@ "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - "path-expression-matcher": ["path-expression-matcher@1.6.2", "", {}, "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ=="], - "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], - "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], @@ -4485,18 +3614,12 @@ "pify": ["pify@5.0.0", "", {}, "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA=="], - "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], - "pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], "playwright": ["playwright@1.62.1", "", { "dependencies": { "playwright-core": "1.62.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg=="], "playwright-core": ["playwright-core@1.62.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw=="], - "plist": ["plist@3.1.1", "", { "dependencies": { "@xmldom/xmldom": "^0.9.10", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA=="], - - "pngjs": ["pngjs@3.4.0", "", {}, "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="], - "points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], "points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="], @@ -4533,8 +3656,6 @@ "powershell-utils": ["powershell-utils@0.2.0", "", {}, "sha512-ZlsFlG7MtSFCoc5xreOvBAozCJ6Pf06opgJjh9ONEv418xpZSAzNjstD36C6+JwOnfSqOW/9uDkqKjezTdxZhw=="], - "preact": ["preact@10.24.2", "", {}, "sha512-1cSoF0aCC8uaARATfrlz4VCBqE8LwZwRfLgkxJOQwAlQt6ayTmi0D9OF7nXid1POI5SZidFuG9CnlXbDfLqY/Q=="], - "prettier": ["prettier@3.9.6", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g=="], "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], @@ -4543,14 +3664,10 @@ "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], - "proc-log": ["proc-log@4.2.0", "", {}, "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA=="], - "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], - "promise": ["promise@8.3.0", "", { "dependencies": { "asap": "~2.0.6" } }, "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg=="], - "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], @@ -4577,16 +3694,12 @@ "pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="], - "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], - "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], "quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], "query-string": ["query-string@9.4.1", "", { "dependencies": { "decode-uri-component": "^0.4.1", "filter-obj": "^5.1.0", "split-on-first": "^3.0.0" } }, "sha512-lSyJeN3RuaG7DZGWThtYRhk96+kEyZ/+doZpERuWbjeFL+Ok3vEat/swU498rAI0NcVt5/RJp8UDuLz7FckxrA=="], - "queue": ["queue@6.0.2", "", { "dependencies": { "inherits": "~2.0.3" } }, "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA=="], - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="], @@ -4613,44 +3726,20 @@ "react-draggable": ["react-draggable@4.7.1", "", { "dependencies": { "clsx": "^2.1.1", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.3.0", "react-dom": ">= 16.3.0" } }, "sha512-wa3tzfFnYt3yaZLuyU58fl1TNunfWfBekDgWhZA1+gb2jnp42wZ0ymuopR6M5kqDYmm4hKmzGlcKWjZf3Zb6RQ=="], - "react-fast-compare": ["react-fast-compare@3.2.2", "", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="], - - "react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="], - "react-grid-layout": ["react-grid-layout@2.2.4", "", { "dependencies": { "clsx": "^2.1.1", "fast-equals": "^4.0.3", "prop-types": "^15.8.1", "react-draggable": "^4.4.6", "react-resizable": "^3.1.3", "resize-observer-polyfill": "^1.5.1" }, "peerDependencies": { "react": ">= 16.3.0", "react-dom": ">= 16.3.0" } }, "sha512-Eb57FsgOMYOfsUGrMI1ku/FFR+dPNPrE8qo+3hwZubpqVSy4GO9v52DeX50Tl3JDYAlCypP4rmw7Vrqk/zOIvA=="], "react-is": ["react-is@19.2.8", "", {}, "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ=="], - "react-native": ["react-native@0.83.4", "", { "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.83.4", "@react-native/codegen": "0.83.4", "@react-native/community-cli-plugin": "0.83.4", "@react-native/gradle-plugin": "0.83.4", "@react-native/js-polyfills": "0.83.4", "@react-native/normalize-colors": "0.83.4", "@react-native/virtualized-lists": "0.83.4", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.32.0", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "hermes-compiler": "0.14.1", "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.83.3", "metro-source-map": "^0.83.3", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.27.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^19.1.1", "react": "^19.2.0" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-H5Wco3UJyY6zZsjoBayY8RM9uiAEQ3FeG4G2NAt+lr9DO43QeqPlVe9xxxYEukMkEmeIhNjR70F6bhXuWArOMQ=="], - - "react-native-gesture-handler": ["react-native-gesture-handler@2.30.1", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-xIUBDo5ktmJs++0fZlavQNvDEE4PsihWhSeJsJtoz4Q6p0MiTM9TgrTgfEgzRR36qGPytFoeq+ShLrVwGdpUdA=="], - - "react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.3.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA=="], - - "react-native-safe-area-context": ["react-native-safe-area-context@5.6.2", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg=="], - - "react-native-screens": ["react-native-screens@4.23.0", "", { "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-XhO3aK0UeLpBn4kLecd+J+EDeRRJlI/Ro9Fze06vo1q163VeYtzfU9QS09/VyDFMWR1qxDC1iazCArTPSFFiPw=="], - - "react-native-svg": ["react-native-svg@15.15.3", "", { "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", "warn-once": "0.1.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-/k4KYwPBLGcx2f5d4FjE+vCScK7QOX14cl2lIASJ28u4slHHtIhL0SZKU7u9qmRBHxTCKPoPBtN6haT1NENJNA=="], - - "react-native-url-polyfill": ["react-native-url-polyfill@2.0.0", "", { "dependencies": { "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "react-native": "*" } }, "sha512-My330Do7/DvKnEvwQc0WdcBnFPploYKp9CYlefDXzIdEaA+PAhDYllkvGeEroEzvc4Kzzj2O4yVdz8v6fjRvhA=="], - "react-reconciler": ["react-reconciler@0.33.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="], "react-redux": ["react-redux@9.3.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g=="], "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], - "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], - - "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], - "react-resizable": ["react-resizable@3.2.0", "", { "dependencies": { "prop-types": "15.x", "react-draggable": "^4.5.0" }, "peerDependencies": { "react": ">= 16.3", "react-dom": ">= 16.3" } }, "sha512-3NKQ0SLZV7rs3LQHeXlOzDSRQfFrkX6TVet77/Qk03zqiZyee37b7N8/gwDJAA8UUjRz7PdWCCy49hcso45SMQ=="], "react-resizable-panels": ["react-resizable-panels@4.12.2", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-NwY5LCo4WrxVvDh0xoMML6EMLPONP/8ckKcIdpnojxexoatZdjLiRqLJQjQK5CPkd4SYiB/2M5BVrjZBQtOO7Q=="], - "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], - "readable-stream": ["readable-stream@2.3.8", "", { "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" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], @@ -4673,12 +3762,6 @@ "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], - "regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="], - - "regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="], - - "regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="], - "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], "regex-empty-conditional-comments": ["regex-empty-conditional-comments@3.1.3", "", {}, "sha512-1bld//jNjpNXXfcuv7Ch7gDaHaK4DU8I7B52E7c20jPjwUJk5X9TpP7BmLr6KtyLUZgKqTzZME+TRL1JzfOROg=="], @@ -4689,12 +3772,6 @@ "regexp-to-ast": ["regexp-to-ast@0.5.0", "", {}, "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw=="], - "regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="], - - "regjsgen": ["regjsgen@0.8.0", "", {}, "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q=="], - - "regjsparser": ["regjsparser@0.13.2", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ=="], - "rehype": ["rehype@13.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "unified": "^11.0.0" } }, "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A=="], "rehype-harden": ["rehype-harden@1.1.8", "", { "dependencies": { "unist-util-visit": "^5.0.0" } }, "sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw=="], @@ -4741,20 +3818,14 @@ "require-in-the-middle": ["require-in-the-middle@7.5.2", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3", "resolve": "^1.22.8" } }, "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ=="], - "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], - "reselect": ["reselect@5.2.0", "", {}, "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="], "resize-observer-polyfill": ["resize-observer-polyfill@1.5.1", "", {}, "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="], "resolve": ["resolve@1.22.12", "", { "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" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], - "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - "resolve-workspace-root": ["resolve-workspace-root@2.0.1", "", {}, "sha512-nR23LHAvaI6aHtMg6RWoaHpdR4D881Nydkzi2CixINyg9T00KgaJdJI6Vwty+Ps8WLxZHuxsS0BseWjxSA4C+w=="], - "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="], "retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="], @@ -4771,8 +3842,6 @@ "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], - "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], - "robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="], "rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="], @@ -4787,8 +3856,6 @@ "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], - "rpc-websockets": ["rpc-websockets@9.3.10", "", { "dependencies": { "@swc/helpers": "^0.5.11", "@types/ws": "^8.2.2", "buffer": "^6.0.3", "eventemitter3": "^5.0.1", "ws": "^8.5.0" }, "optionalDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^6.0.0" } }, "sha512-QT5PQ6LiWhA5RCS93oWwgxU4XzQltkYm8C3aTmmKEgj0HolGRo3VbdzELw7CEV35l9T7Amha8Vnr4rCfSjVP+w=="], - "rrdom": ["rrdom@2.1.1", "", { "dependencies": { "rrweb-snapshot": "^2.1.1" } }, "sha512-VBkTF3bGNqcZjqnbo/gKi9GVQPIxiG0dofw7CQdAZQFQ2juJdt2YKIf3oS9QudL8HTpGh9bz5TUN+3amBn1Geg=="], "rrweb": ["rrweb@2.1.1", "", { "dependencies": { "@rrweb/types": "^2.1.1", "@rrweb/utils": "^2.1.1", "@types/css-font-loading-module": "0.0.7", "@xstate/fsm": "^1.4.0", "base64-arraybuffer": "^1.0.1", "mitt": "^3.0.0", "rrdom": "^2.1.1", "rrweb-snapshot": "^2.1.1" } }, "sha512-ToxhJg3SsrAhw+/DPhI/2iiwZQIrGK5BGkZ0kHn4qUExcvQYRaolkciD1FWX2+r6vf1IxFyhsoRY18h3D+XoCg=="], @@ -4825,28 +3892,18 @@ "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - "serialize-error": ["serialize-error@2.1.0", "", {}, "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw=="], - "seroval": ["seroval@1.6.0", "", {}, "sha512-TBwwKfscTEgnBEWmYKKeCcmCGmrJi0LV6qNUY//WBA3MDesh/zfn+KOMq/ckpxM4gZ0ouAE706A1eenekM2sug=="], "seroval-plugins": ["seroval-plugins@1.6.0", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-CbR5DP5DPicpd9RwRUzka7hi4x1577eGFXJVBW409LXqqJNst99JSUTZ8CXcnskQX7laPOWpmzliq0gBZEGlrQ=="], "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], - "server-only": ["server-only@0.0.1", "", {}, "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="], - - "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], - "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - "sf-symbols-typescript": ["sf-symbols-typescript@2.2.0", "", {}, "sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw=="], - "sha.js": ["sha.js@2.4.11", "", { "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" }, "bin": { "sha.js": "./bin.js" } }, "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ=="], - "shallowequal": ["shallowequal@1.1.0", "", {}, "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="], - "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -4869,20 +3926,12 @@ "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "simple-plist": ["simple-plist@1.3.1", "", { "dependencies": { "bplist-creator": "0.1.0", "bplist-parser": "0.3.1", "plist": "^3.0.5" } }, "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw=="], - - "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], - "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], "sitemap": ["sitemap@9.0.1", "", { "dependencies": { "@types/node": "^24.9.2", "@types/sax": "^1.2.1", "arg": "^5.0.0", "sax": "^1.4.1" }, "bin": { "sitemap": "dist/esm/cli.js" } }, "sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ=="], - "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], - "slugify": ["slugify@1.6.9", "", {}, "sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg=="], - "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], "smol-toml": ["smol-toml@1.7.1", "", {}, "sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ=="], @@ -4919,14 +3968,8 @@ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="], - - "stacktrace-parser": ["stacktrace-parser@0.1.11", "", { "dependencies": { "type-fest": "^0.7.1" } }, "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg=="], - "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], - "standard-navigation": ["standard-navigation@0.0.8", "", { "peerDependencies": { "react": "*" } }, "sha512-TyVbo7INUDWtsUWDFn8RR7kwR87U0S4xHfLfbbnyeC581TmmyqQ+eM+nPw8rQTSD8QitRVcYfPaSHr/QJiUy1g=="], - "standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="], "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], @@ -4935,20 +3978,12 @@ "stdin-discarder": ["stdin-discarder@0.3.2", "", {}, "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A=="], - "stream-buffers": ["stream-buffers@2.2.0", "", {}, "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg=="], - - "stream-chain": ["stream-chain@2.2.5", "", {}, "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA=="], - - "stream-json": ["stream-json@1.9.1", "", { "dependencies": { "stream-chain": "^2.2.5" } }, "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw=="], - "stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="], "streamdown": ["streamdown@2.5.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "mermaid": "^11.12.2", "rehype-harden": "^1.1.8", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.3.0", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-/tTnURfIOxZK/pqJAxsfCvETG/XCJHoWnk3jq9xLcuz6CSpnjjuxSRBTTL4PKGhxiZQf0lqPxGhImdpwcZ2XwA=="], "streamx": ["streamx@2.28.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw=="], - "strict-uri-encode": ["strict-uri-encode@2.0.0", "", {}, "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="], - "string-character-is-astral-surrogate": ["string-character-is-astral-surrogate@3.1.3", "", {}, "sha512-sqj1xo8SMWp6WG/pUYoIiYNgSHQTTuJA/BmdpnvoAoo121RVOTJJQ7V1RKAOwT+vVOYhwiLydb9ul5tgnMPY9w=="], "string-collapse-leading-whitespace": ["string-collapse-leading-whitespace@7.1.3", "", {}, "sha512-bCgNODedM9daANnnNPOHuz++qUeVHgsPMghNPznbLLnBviNyuKt9vzHIa6OGTSpWzJUO4Rlffu4+RD47pSTq3g=="], @@ -4985,8 +4020,6 @@ "strnum": ["strnum@2.4.1", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg=="], - "structured-headers": ["structured-headers@0.4.1", "", {}, "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg=="], - "stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="], "stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="], @@ -5005,12 +4038,8 @@ "sugar-high": ["sugar-high@1.2.1", "", {}, "sha512-C2E0iSC0Gwv+7t32ATFxgiH6fxskoSfd/nF5z11pQSrgJHYjY8/U11K+X0izV9ZyPNPwaomtn6uF7y11MxVNQA=="], - "superstruct": ["superstruct@2.0.2", "", {}, "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A=="], - "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - "supports-hyperlinks": ["supports-hyperlinks@2.3.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA=="], - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], "svgo": ["svgo@4.0.2", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng=="], @@ -5035,22 +4064,14 @@ "teex": ["teex@1.0.1", "", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg=="], - "terminal-link": ["terminal-link@2.1.1", "", { "dependencies": { "ansi-escapes": "^4.2.1", "supports-hyperlinks": "^2.0.0" } }, "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ=="], - "terminal-size": ["terminal-size@4.0.1", "", {}, "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ=="], "terser": ["terser@5.49.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA=="], - "test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="], - "test-mixer": ["test-mixer@4.2.3", "", { "dependencies": { "object-boolean-combinations": "^6.2.3", "rfdc": "^1.4.1" } }, "sha512-6sBlzwiDARX7Qp13MwYygwjeNrtlkbHj+6t/+1OnROQWLX+rpquOc+XXbjVs5xYUtltNo6ZpvFM4nNWtOdInHw=="], "text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="], - "text-encoding-utf-8": ["text-encoding-utf-8@1.0.2", "", {}, "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg=="], - - "throat": ["throat@5.0.0", "", {}, "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA=="], - "throttle-debounce": ["throttle-debounce@5.0.2", "", {}, "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A=="], "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], @@ -5071,16 +4092,12 @@ "tldts-core": ["tldts-core@7.4.10", "", {}, "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw=="], - "tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="], - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "to-rotated": ["to-rotated@1.0.0", "", {}, "sha512-KsEID8AfgUy+pxVRLsWp0VzCa69wxzUDZnzGbyIST/bcgcrMvTYoFBX/QORH4YApoD89EDuUovx4BTdpOn319Q=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - "toqr": ["toqr@0.1.1", "", {}, "sha512-FWAPzCIHZHnrE/5/w9MPk0kK25hSQSH2IKhYh9PyjS3SG/+IEMvlwIHbhz+oF7xl54I+ueZlVnMjyzdSwLmAwA=="], - "tough-cookie": ["tough-cookie@6.0.2", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA=="], "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], @@ -5105,8 +4122,6 @@ "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], - "type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="], - "type-fest": ["type-fest@5.8.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA=="], "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], @@ -5135,14 +4150,6 @@ "unhead": ["unhead@3.3.1", "", { "dependencies": { "hookable": "^6.1.1", "unplugin": "^3.3.0" }, "peerDependencies": { "vite": ">=6.4.2" }, "optionalPeers": ["vite"] }, "sha512-eqHlbLyuvIXw898WopQmosTml4PdYW5ZhXDom/sf7ysAqQB9uvYrw2/dNvbrmkJTufSkmNmgGCjoQcvoT2mS/A=="], - "unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="], - - "unicode-match-property-ecmascript": ["unicode-match-property-ecmascript@2.0.0", "", { "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" } }, "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q=="], - - "unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="], - - "unicode-property-aliases-ecmascript": ["unicode-property-aliases-ecmascript@2.2.0", "", {}, "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ=="], - "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], @@ -5177,8 +4184,6 @@ "universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="], - "uniwind": ["uniwind@1.10.0", "", { "dependencies": { "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "culori": "4.0.2", "lightningcss": "1.30.1" }, "peerDependencies": { "@expo/metro-config": "*", "metro": "*", "metro-cache": "*", "metro-transform-worker": "*", "react": ">=19.0.0", "react-native": ">=0.81.0", "tailwindcss": ">=4" }, "optionalPeers": ["@expo/metro-config", "metro-transform-worker"], "bin": { "uniwind": "dist/cli/index.mjs" } }, "sha512-YUCM90mqtNn172XQmC2SOmRK502HgL0NK5jdd9bJQhqGhJNvmzK+G894lOkzX94SLOGlDe0te4AaYLwyBSW+Cg=="], - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], "unplugin": ["unplugin@3.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.4", "webpack-virtual-modules": "^0.6.2" }, "peerDependencies": { "@farmfe/core": "*", "@rspack/core": "*", "bun-types-no-globals": "*", "esbuild": "*", "rolldown": "*", "rollup": "*", "unloader": "*", "vite": "*", "webpack": "*" }, "optionalPeers": ["@farmfe/core", "@rspack/core", "bun-types-no-globals", "esbuild", "rolldown", "rollup", "unloader", "vite", "webpack"] }, "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg=="], @@ -5197,32 +4202,20 @@ "uqr": ["uqr@0.1.3", "", {}, "sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA=="], - "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], - - "use-latest-callback": ["use-latest-callback@0.2.6", "", { "peerDependencies": { "react": ">=16.8" } }, "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg=="], - - "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], - "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], "utf-8-validate": ["utf-8-validate@6.0.6", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], - "uuid": ["uuid@14.0.1", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew=="], "valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="], "valid-data-url": ["valid-data-url@3.0.1", "", {}, "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA=="], - "validate-npm-package-name": ["validate-npm-package-name@5.0.1", "", {}, "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ=="], - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - "vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="], - "verkit": ["verkit@0.3.1", "", {}, "sha512-w2Eo8LSIIoW7qxNBzT7/17k+bh8plXo7G3dHjEIDqPlnluhzaxr9JX8F28VSYEtDvc1/a3WBDih6xNUZseebXg=="], "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], @@ -5233,8 +4226,6 @@ "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], - "viem": ["viem@2.55.10", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.14.33", "ws": "8.21.0" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-Q9Ba+/ma81U2M5o5P2AQ7Ux8rTIwmCZvUcr8rKdQ22bV0IBFHllM2m5gWDP8hFaUN2nH2oW3QG44amRazflYNQ=="], - "vite": ["vite@8.2.0", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.23", "rolldown": "~1.2.0", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ=="], "vite-tsconfig-paths": ["vite-tsconfig-paths@6.1.1", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg=="], @@ -5245,8 +4236,6 @@ "vitest-evals": ["vitest-evals@0.4.1", "", { "peerDependencies": { "tinyrainbow": "*", "vitest": "*" } }, "sha512-4QPnV+H5Sffhtm2D5kofi9W5Dk+PrSsdSqf2eonUMFnK8bcF5mcQjHcNZ04iOQWilZYWYs67OPjQdU5Gbc7Mnw=="], - "vlq": ["vlq@1.0.1", "", {}, "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w=="], - "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], "vue": ["vue@3.5.41", "", { "dependencies": { "@vue/compiler-dom": "3.5.41", "@vue/compiler-sfc": "3.5.41", "@vue/runtime-dom": "3.5.41", "@vue/server-renderer": "3.5.41", "@vue/shared": "3.5.41" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg=="], @@ -5259,12 +4248,6 @@ "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="], - "walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="], - - "warn-once": ["warn-once@0.1.1", "", {}, "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q=="], - - "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], - "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "web-resource-inliner": ["web-resource-inliner@8.0.0", "", { "dependencies": { "ansi-colors": "^4.1.1", "escape-goat": "^3.0.0", "htmlparser2": "^9.1.0", "mime": "^2.4.6", "valid-data-url": "^3.0.0" } }, "sha512-Ezr98sqXW/+OCGoUEXuOKVR+oVFlSdn1tIySEEJdiSAw4IjrW8hQkwARSSBJTSB5Us5dnytDgL0ZDliAYBhaNA=="], @@ -5277,22 +4260,14 @@ "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], - "whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="], - "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], "whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="], - "whatwg-url-minimum": ["whatwg-url-minimum@0.1.2", "", {}, "sha512-XPEm0XFQWNVG292lII1PrRRJl3sItrs7CettZ4ncYxuDVpLyy+NwlGyut2hXI0JswcJUxeCH+CyOJK0ZzAXD6A=="], - - "whatwg-url-without-unicode": ["whatwg-url-without-unicode@8.0.0-3", "", { "dependencies": { "buffer": "^5.4.3", "punycode": "^2.1.1", "webidl-conversions": "^5.0.0" } }, "sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig=="], - "when-exit": ["when-exit@2.1.5", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], - "which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="], "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], @@ -5309,22 +4284,14 @@ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "write-file-atomic": ["write-file-atomic@4.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" } }, "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg=="], - "ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="], "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], - "xcode": ["xcode@3.0.1", "", { "dependencies": { "simple-plist": "^1.1.0", "uuid": "^7.0.3" } }, "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA=="], - "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], "xml-naming": ["xml-naming@0.3.0", "", {}, "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ=="], - "xml2js": ["xml2js@0.6.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w=="], - - "xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], - "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], @@ -5393,48 +4360,30 @@ "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], - "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/core/@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="], - "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/generator/@babel/parser": ["@babel/parser@8.0.4", "", { "dependencies": { "@babel/types": "^8.0.4" }, "bin": "./bin/babel-parser.js" }, "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g=="], - "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/generator/@babel/types": ["@babel/types@8.0.4", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0", "@babel/helper-validator-identifier": "^8.0.4" } }, "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g=="], - "@babel/helper-create-regexp-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "@babel/highlight/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/plugin-transform-runtime/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@base-org/account/clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="], - - "@base-org/account/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], - - "@base-org/account/zustand": ["zustand@5.0.3", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg=="], + "@babel/traverse/@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="], "@chevrotain/cst-dts-gen/@chevrotain/types": ["@chevrotain/types@10.5.0", "", {}, "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A=="], - "@chevrotain/cst-dts-gen/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - "@chevrotain/gast/@chevrotain/types": ["@chevrotain/types@10.5.0", "", {}, "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A=="], - "@chevrotain/gast/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - - "@clerk/clerk-js/@clerk/shared": ["@clerk/shared@4.25.10", "", { "dependencies": { "@tanstack/query-core": "^5.100.6", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.7" }, "peerDependencies": { "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-Vjqk74nQGJ8y+cm+eSshoBzRvBhIzuKLOs3Gt13gEHdyQ6yV798Dl2SrBKt0jw2u6I1tOcD3s8/i9P4yJYo0dw=="], - - "@clerk/clerk-js/@swc/helpers": ["@swc/helpers@0.5.21", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg=="], - - "@clerk/expo/@clerk/shared": ["@clerk/shared@4.25.10", "", { "dependencies": { "@tanstack/query-core": "^5.100.6", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.7" }, "peerDependencies": { "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-Vjqk74nQGJ8y+cm+eSshoBzRvBhIzuKLOs3Gt13gEHdyQ6yV798Dl2SrBKt0jw2u6I1tOcD3s8/i9P4yJYo0dw=="], - - "@clerk/react/@clerk/shared": ["@clerk/shared@4.25.10", "", { "dependencies": { "@tanstack/query-core": "^5.100.6", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.7" }, "peerDependencies": { "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-Vjqk74nQGJ8y+cm+eSshoBzRvBhIzuKLOs3Gt13gEHdyQ6yV798Dl2SrBKt0jw2u6I1tOcD3s8/i9P4yJYo0dw=="], - "@clerk/shared/csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "@clerk/shared/std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], - "@coinbase/wallet-sdk/clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="], - "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], "@effect/platform-node-shared/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], @@ -5447,98 +4396,20 @@ "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], - "@expo/cli/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], - - "@expo/cli/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "@expo/cli/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], - - "@expo/cli/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], - - "@expo/cli/ora": ["ora@3.4.0", "", { "dependencies": { "chalk": "^2.4.2", "cli-cursor": "^2.1.0", "cli-spinners": "^2.0.0", "log-symbols": "^2.2.0", "strip-ansi": "^5.2.0", "wcwidth": "^1.0.1" } }, "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg=="], - - "@expo/cli/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - - "@expo/cli/send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], - - "@expo/cli/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "@expo/cli/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], - - "@expo/config/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], - - "@expo/config-plugins/@expo/json-file": ["@expo/json-file@10.0.16", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "json5": "^2.2.3" } }, "sha512-fcVkWEj+hLuP2yt5W0aw6LmDRqSPWDLUSxOMcmFeV+algmIF59sQVKCwB9btjQLd4V6x9N0pISkQEkBubUHrCw=="], - - "@expo/config-plugins/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "@expo/config-plugins/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], - - "@expo/devcert/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - - "@expo/devtools/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "@expo/env/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "@expo/fingerprint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "@expo/fingerprint/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], - - "@expo/image-utils/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "@expo/local-build-cache-provider/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "@expo/metro-config/@expo/json-file": ["@expo/json-file@10.0.16", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "json5": "^2.2.3" } }, "sha512-fcVkWEj+hLuP2yt5W0aw6LmDRqSPWDLUSxOMcmFeV+algmIF59sQVKCwB9btjQLd4V6x9N0pISkQEkBubUHrCw=="], - - "@expo/metro-config/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "@expo/metro-config/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], - - "@expo/metro-runtime/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - - "@expo/package-manager/@expo/json-file": ["@expo/json-file@11.0.1", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "json5": "^2.2.3" } }, "sha512-zxHWj4MKKMAL29ZQSY/Fssx4Thluk40JmuGNaeS078wy/NhlFhnVi+rHHunulE3xJAJ0CM73m8VK2+GkF9eRwQ=="], - - "@expo/package-manager/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "@expo/package-manager/ora": ["ora@3.4.0", "", { "dependencies": { "chalk": "^2.4.2", "cli-cursor": "^2.1.0", "cli-spinners": "^2.0.0", "log-symbols": "^2.2.0", "strip-ansi": "^5.2.0", "wcwidth": "^1.0.1" } }, "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg=="], - - "@expo/prebuild-config/@react-native/normalize-colors": ["@react-native/normalize-colors@0.83.10", "", {}, "sha512-dWgqcxaBy27oLx9tndcTF0917vbLOOstyNjYD58J6Z7YxkEZSaKG6+A+3I1KV6O1Xg2D69QThp4t6ezoC3NiGA=="], - - "@expo/xcpretty/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "@img/sharp-freebsd-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.2", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw=="], "@img/sharp-webcontainers-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.2", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw=="], - "@inlang/json-types/@sinclair/typebox": ["@sinclair/typebox@0.31.30", "", {}, "sha512-MGsM7bVmHg3sUKCphlu3SGQ+T+5JTbygZWYArfKQCW5anJeACHeqLhcnf+R7XlCWZ+QR3C8JTBx3kCKJTN69ow=="], - - "@inlang/language-tag/@sinclair/typebox": ["@sinclair/typebox@0.31.30", "", {}, "sha512-MGsM7bVmHg3sUKCphlu3SGQ+T+5JTbygZWYArfKQCW5anJeACHeqLhcnf+R7XlCWZ+QR3C8JTBx3kCKJTN69ow=="], - - "@inlang/message/@sinclair/typebox": ["@sinclair/typebox@0.31.30", "", {}, "sha512-MGsM7bVmHg3sUKCphlu3SGQ+T+5JTbygZWYArfKQCW5anJeACHeqLhcnf+R7XlCWZ+QR3C8JTBx3kCKJTN69ow=="], - - "@inlang/message-lint-rule/@sinclair/typebox": ["@sinclair/typebox@0.31.30", "", {}, "sha512-MGsM7bVmHg3sUKCphlu3SGQ+T+5JTbygZWYArfKQCW5anJeACHeqLhcnf+R7XlCWZ+QR3C8JTBx3kCKJTN69ow=="], - - "@inlang/module/@sinclair/typebox": ["@sinclair/typebox@0.31.30", "", {}, "sha512-MGsM7bVmHg3sUKCphlu3SGQ+T+5JTbygZWYArfKQCW5anJeACHeqLhcnf+R7XlCWZ+QR3C8JTBx3kCKJTN69ow=="], - "@inlang/paraglide-js/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], "@inlang/paraglide-unplugin/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "@inlang/paraglide-unplugin/unplugin": ["unplugin@1.16.1", "", { "dependencies": { "acorn": "^8.14.0", "webpack-virtual-modules": "^0.6.2" } }, "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w=="], - "@inlang/plugin/@sinclair/typebox": ["@sinclair/typebox@0.31.30", "", {}, "sha512-MGsM7bVmHg3sUKCphlu3SGQ+T+5JTbygZWYArfKQCW5anJeACHeqLhcnf+R7XlCWZ+QR3C8JTBx3kCKJTN69ow=="], - - "@inlang/project-settings/@sinclair/typebox": ["@sinclair/typebox@0.31.30", "", {}, "sha512-MGsM7bVmHg3sUKCphlu3SGQ+T+5JTbygZWYArfKQCW5anJeACHeqLhcnf+R7XlCWZ+QR3C8JTBx3kCKJTN69ow=="], - "@inlang/recommend-ninja/@inlang/sdk": ["@inlang/sdk@0.36.3", "", { "dependencies": { "@inlang/json-types": "1.1.0", "@inlang/language-tag": "1.5.1", "@inlang/message": "2.1.0", "@inlang/message-lint-rule": "1.4.7", "@inlang/module": "1.2.14", "@inlang/plugin": "2.4.14", "@inlang/project-settings": "2.4.2", "@inlang/result": "1.1.0", "@inlang/translatable": "1.3.1", "@lix-js/client": "2.2.1", "@lix-js/fs": "2.2.0", "@sinclair/typebox": "^0.31.17", "debug": "^4.3.4", "dedent": "1.5.1", "deepmerge-ts": "^5.1.0", "murmurhash3js": "^3.0.1", "solid-js": "1.6.12", "throttle-debounce": "^5.0.0" } }, "sha512-wjsavc44H24v74tdEQ13FqZZcr43T106oEfHJnBLzEP55Zz2JJWABLund+DEdosZx+9E8mJBEW5JlVnlBwP3Zw=="], - "@inlang/recommend-ninja/@sinclair/typebox": ["@sinclair/typebox@0.31.30", "", {}, "sha512-MGsM7bVmHg3sUKCphlu3SGQ+T+5JTbygZWYArfKQCW5anJeACHeqLhcnf+R7XlCWZ+QR3C8JTBx3kCKJTN69ow=="], - "@inlang/recommend-sherlock/@inlang/sdk": ["@inlang/sdk@0.36.4", "", { "dependencies": { "@inlang/json-types": "1.1.0", "@inlang/language-tag": "1.5.1", "@inlang/message": "2.1.0", "@inlang/message-lint-rule": "1.4.7", "@inlang/module": "1.2.14", "@inlang/plugin": "2.4.14", "@inlang/project-settings": "2.4.2", "@inlang/result": "1.1.0", "@inlang/translatable": "1.3.1", "@lix-js/client": "2.2.1", "@lix-js/fs": "2.2.0", "@sinclair/typebox": "^0.31.17", "debug": "^4.3.4", "dedent": "1.5.1", "deepmerge-ts": "^5.1.0", "murmurhash3js": "^3.0.1", "solid-js": "1.6.12", "throttle-debounce": "^5.0.0" } }, "sha512-fTr0mkDx2ViZt/8lxaF9Mxj3m8LaqIhcjMJy+CdHREMc9UvpUhGLB7elMp061YysxnN1CFccAgLRug5VWK3yWw=="], - "@inlang/recommend-sherlock/@sinclair/typebox": ["@sinclair/typebox@0.31.30", "", {}, "sha512-MGsM7bVmHg3sUKCphlu3SGQ+T+5JTbygZWYArfKQCW5anJeACHeqLhcnf+R7XlCWZ+QR3C8JTBx3kCKJTN69ow=="], - - "@inlang/sdk/@sinclair/typebox": ["@sinclair/typebox@0.31.30", "", {}, "sha512-MGsM7bVmHg3sUKCphlu3SGQ+T+5JTbygZWYArfKQCW5anJeACHeqLhcnf+R7XlCWZ+QR3C8JTBx3kCKJTN69ow=="], - "@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], @@ -5547,24 +4418,8 @@ "@internationalized/number/@swc/helpers": ["@swc/helpers@0.5.21", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg=="], - "@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], - - "@istanbuljs/load-nyc-config/js-yaml": ["js-yaml@3.15.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag=="], - - "@jest/environment/@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], - - "@jest/fake-timers/@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], - - "@jest/transform/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "@jest/types/@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], - - "@jest/types/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "@libsql/isomorphic-ws/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], - "@lix-js/client/ignore": ["ignore@5.3.1", "", {}, "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw=="], - "@lix-js/fs/typescript": ["typescript@5.2.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w=="], "@maizzle/framework/@clack/core": ["@clack/core@1.4.3", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ=="], @@ -5605,8 +4460,6 @@ "@maple/landing/typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], - "@noble/curves/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - "@octokit/app/@octokit/core": ["@octokit/core@5.2.2", "", { "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.0.0", "before-after-hook": "^2.2.0", "universal-user-agent": "^6.0.0" } }, "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg=="], "@octokit/app/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@9.2.2", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ=="], @@ -5669,36 +4522,30 @@ "@octokit/request/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.1.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ=="], - - "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.1.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ=="], - "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.1.0", "", { "dependencies": { "@opentelemetry/core": "2.1.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1CJjf3LCvoefUOgegxi8h6r4B/wLSzInyhGP2UmIBYNlo4Qk5CZ73e1eEyWmfXvFtm1ybkmfb2DqWvspsYLrWw=="], "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.1.0", "", { "dependencies": { "@opentelemetry/core": "2.1.0", "@opentelemetry/resources": "2.1.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-uTX9FBlVQm4S2gVQO1sb5qyBLq/FPjbp+tmGoxu4tIgtYGmBYB44+KX/725RFDe30yBSaA9Ml9fqphe1hbUyLQ=="], - "@opentelemetry/instrumentation-fetch/@opentelemetry/core": ["@opentelemetry/core@2.1.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ=="], - "@opentelemetry/instrumentation-fetch/@opentelemetry/sdk-trace-web": ["@opentelemetry/sdk-trace-web@2.1.0", "", { "dependencies": { "@opentelemetry/core": "2.1.0", "@opentelemetry/sdk-trace-base": "2.1.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-2F6ZuZFmJg4CdhRPP8+60DkvEwGLCiU3ffAkgnnqe/ALGEBqGa0HrZaNWFGprXWVivrYHpXhr7AEfasgLZD71g=="], - "@opentelemetry/otlp-exporter-base/@opentelemetry/core": ["@opentelemetry/core@2.1.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ=="], - - "@opentelemetry/otlp-transformer/@opentelemetry/core": ["@opentelemetry/core@2.1.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ=="], - "@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.1.0", "", { "dependencies": { "@opentelemetry/core": "2.1.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1CJjf3LCvoefUOgegxi8h6r4B/wLSzInyhGP2UmIBYNlo4Qk5CZ73e1eEyWmfXvFtm1ybkmfb2DqWvspsYLrWw=="], "@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.1.0", "", { "dependencies": { "@opentelemetry/core": "2.1.0", "@opentelemetry/resources": "2.1.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-uTX9FBlVQm4S2gVQO1sb5qyBLq/FPjbp+tmGoxu4tIgtYGmBYB44+KX/725RFDe30yBSaA9Ml9fqphe1hbUyLQ=="], "@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.6.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="], - "@opentelemetry/sdk-logs/@opentelemetry/core": ["@opentelemetry/core@2.1.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ=="], + "@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.10.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ=="], "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.1.0", "", { "dependencies": { "@opentelemetry/core": "2.1.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1CJjf3LCvoefUOgegxi8h6r4B/wLSzInyhGP2UmIBYNlo4Qk5CZ73e1eEyWmfXvFtm1ybkmfb2DqWvspsYLrWw=="], - "@opentelemetry/sdk-metrics/@opentelemetry/core": ["@opentelemetry/core@2.1.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ=="], - "@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.1.0", "", { "dependencies": { "@opentelemetry/core": "2.1.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1CJjf3LCvoefUOgegxi8h6r4B/wLSzInyhGP2UmIBYNlo4Qk5CZ73e1eEyWmfXvFtm1ybkmfb2DqWvspsYLrWw=="], + "@opentelemetry/sdk-trace/@opentelemetry/core": ["@opentelemetry/core@2.10.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ=="], + + "@opentelemetry/sdk-trace-base/@opentelemetry/core": ["@opentelemetry/core@2.10.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ=="], + + "@opentelemetry/sdk-trace-web/@opentelemetry/core": ["@opentelemetry/core@2.10.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ=="], + "@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="], "@oxc-parser/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], @@ -5719,154 +4566,8 @@ "@prisma/dev/std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], - "@react-native/babel-plugin-codegen/@react-native/codegen": ["@react-native/codegen@0.83.10", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.32.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" } }, "sha512-rTcuuwRDPLrBmhXc9vAr2gispQFCEqd2WVPh2+N5eV80p8tf9mHy7CoFnmPy8A8csK6HGlANaD9SMPvccQjh4g=="], - - "@react-native/babel-preset/react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], - - "@react-native/codegen/@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="], - - "@react-native/community-cli-plugin/@react-native/dev-middleware": ["@react-native/dev-middleware@0.83.4", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.83.4", "@react-native/debugger-shell": "0.83.4", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^4.4.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", "ws": "^7.5.10" } }, "sha512-3s9nXZc/kj986nI2RPqxiIJeTS3o7pvZDxbHu7GE9WVIGX9YucA1l/tEiXd7BAm3TBFOfefDOT08xD46wH+R3Q=="], - - "@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], - - "@react-native/dev-middleware/serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], - - "@react-navigation/core/query-string": ["query-string@7.1.3", "", { "dependencies": { "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" } }, "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg=="], - "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], - "@scure/bip32/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - - "@scure/bip39/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - - "@solana-mobile/wallet-adapter-mobile/@react-native-async-storage/async-storage": ["@react-native-async-storage/async-storage@1.24.0", "", { "dependencies": { "merge-options": "^3.0.4" }, "peerDependencies": { "react-native": "^0.0.0-0 || >=0.60 <1.0" } }, "sha512-W4/vbwUOYOjco0x3toB8QCr7EjIP6nE9G7o8PMguvvjYT5Awg09lyV4enACRx4s++PPulBiBSjL0KTFx2u0Z/g=="], - - "@solana-mobile/wallet-standard-mobile/@react-native-async-storage/async-storage": ["@react-native-async-storage/async-storage@1.24.0", "", { "dependencies": { "merge-options": "^3.0.4" }, "peerDependencies": { "react-native": "^0.0.0-0 || >=0.60 <1.0" } }, "sha512-W4/vbwUOYOjco0x3toB8QCr7EjIP6nE9G7o8PMguvvjYT5Awg09lyV4enACRx4s++PPulBiBSjL0KTFx2u0Z/g=="], - - "@solana/accounts/@solana/codecs-core": ["@solana/codecs-core@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-nfAl9OMGo4HanIMxGsQoVB7BxMoqBCYEUxl8oEAZZ09pDxnaXQZkTRXEwPPccag37XfW1ciPd1vWPKwB2b0HHQ=="], - - "@solana/accounts/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/addresses/@solana/codecs-core": ["@solana/codecs-core@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-nfAl9OMGo4HanIMxGsQoVB7BxMoqBCYEUxl8oEAZZ09pDxnaXQZkTRXEwPPccag37XfW1ciPd1vWPKwB2b0HHQ=="], - - "@solana/addresses/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/assertions/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/buffer-layout/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - - "@solana/codecs/@solana/codecs-core": ["@solana/codecs-core@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-nfAl9OMGo4HanIMxGsQoVB7BxMoqBCYEUxl8oEAZZ09pDxnaXQZkTRXEwPPccag37XfW1ciPd1vWPKwB2b0HHQ=="], - - "@solana/codecs/@solana/codecs-numbers": ["@solana/codecs-numbers@6.10.0", "", { "dependencies": { "@solana/codecs-core": "6.10.0", "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-CcM+wX4zOiA9zkh8A7t1787A0Ehgmu5+6Z2tKoHew6cNw/dkaUTPa8JnNHbvfsLC8dfHC1BhAEJl86sKmRsfkQ=="], - - "@solana/codecs-data-structures/@solana/codecs-core": ["@solana/codecs-core@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-nfAl9OMGo4HanIMxGsQoVB7BxMoqBCYEUxl8oEAZZ09pDxnaXQZkTRXEwPPccag37XfW1ciPd1vWPKwB2b0HHQ=="], - - "@solana/codecs-data-structures/@solana/codecs-numbers": ["@solana/codecs-numbers@6.10.0", "", { "dependencies": { "@solana/codecs-core": "6.10.0", "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-CcM+wX4zOiA9zkh8A7t1787A0Ehgmu5+6Z2tKoHew6cNw/dkaUTPa8JnNHbvfsLC8dfHC1BhAEJl86sKmRsfkQ=="], - - "@solana/codecs-data-structures/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/codecs-strings/@solana/codecs-core": ["@solana/codecs-core@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-nfAl9OMGo4HanIMxGsQoVB7BxMoqBCYEUxl8oEAZZ09pDxnaXQZkTRXEwPPccag37XfW1ciPd1vWPKwB2b0HHQ=="], - - "@solana/codecs-strings/@solana/codecs-numbers": ["@solana/codecs-numbers@6.10.0", "", { "dependencies": { "@solana/codecs-core": "6.10.0", "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-CcM+wX4zOiA9zkh8A7t1787A0Ehgmu5+6Z2tKoHew6cNw/dkaUTPa8JnNHbvfsLC8dfHC1BhAEJl86sKmRsfkQ=="], - - "@solana/codecs-strings/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/errors/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], - - "@solana/fixed-points/@solana/codecs-core": ["@solana/codecs-core@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-nfAl9OMGo4HanIMxGsQoVB7BxMoqBCYEUxl8oEAZZ09pDxnaXQZkTRXEwPPccag37XfW1ciPd1vWPKwB2b0HHQ=="], - - "@solana/fixed-points/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/instruction-plans/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/instructions/@solana/codecs-core": ["@solana/codecs-core@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-nfAl9OMGo4HanIMxGsQoVB7BxMoqBCYEUxl8oEAZZ09pDxnaXQZkTRXEwPPccag37XfW1ciPd1vWPKwB2b0HHQ=="], - - "@solana/instructions/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/keys/@solana/codecs-core": ["@solana/codecs-core@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-nfAl9OMGo4HanIMxGsQoVB7BxMoqBCYEUxl8oEAZZ09pDxnaXQZkTRXEwPPccag37XfW1ciPd1vWPKwB2b0HHQ=="], - - "@solana/keys/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/kit/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/offchain-messages/@solana/codecs-core": ["@solana/codecs-core@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-nfAl9OMGo4HanIMxGsQoVB7BxMoqBCYEUxl8oEAZZ09pDxnaXQZkTRXEwPPccag37XfW1ciPd1vWPKwB2b0HHQ=="], - - "@solana/offchain-messages/@solana/codecs-numbers": ["@solana/codecs-numbers@6.10.0", "", { "dependencies": { "@solana/codecs-core": "6.10.0", "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-CcM+wX4zOiA9zkh8A7t1787A0Ehgmu5+6Z2tKoHew6cNw/dkaUTPa8JnNHbvfsLC8dfHC1BhAEJl86sKmRsfkQ=="], - - "@solana/offchain-messages/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/options/@solana/codecs-core": ["@solana/codecs-core@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-nfAl9OMGo4HanIMxGsQoVB7BxMoqBCYEUxl8oEAZZ09pDxnaXQZkTRXEwPPccag37XfW1ciPd1vWPKwB2b0HHQ=="], - - "@solana/options/@solana/codecs-numbers": ["@solana/codecs-numbers@6.10.0", "", { "dependencies": { "@solana/codecs-core": "6.10.0", "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-CcM+wX4zOiA9zkh8A7t1787A0Ehgmu5+6Z2tKoHew6cNw/dkaUTPa8JnNHbvfsLC8dfHC1BhAEJl86sKmRsfkQ=="], - - "@solana/options/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/program-client-core/@solana/codecs-core": ["@solana/codecs-core@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-nfAl9OMGo4HanIMxGsQoVB7BxMoqBCYEUxl8oEAZZ09pDxnaXQZkTRXEwPPccag37XfW1ciPd1vWPKwB2b0HHQ=="], - - "@solana/program-client-core/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/programs/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/rpc/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/rpc-api/@solana/codecs-core": ["@solana/codecs-core@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-nfAl9OMGo4HanIMxGsQoVB7BxMoqBCYEUxl8oEAZZ09pDxnaXQZkTRXEwPPccag37XfW1ciPd1vWPKwB2b0HHQ=="], - - "@solana/rpc-api/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/rpc-spec/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/rpc-subscriptions/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/rpc-subscriptions-channel-websocket/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/rpc-subscriptions-channel-websocket/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], - - "@solana/rpc-subscriptions-spec/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/rpc-transformers/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/rpc-transport-http/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/rpc-transport-http/undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], - - "@solana/rpc-types/@solana/codecs-core": ["@solana/codecs-core@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-nfAl9OMGo4HanIMxGsQoVB7BxMoqBCYEUxl8oEAZZ09pDxnaXQZkTRXEwPPccag37XfW1ciPd1vWPKwB2b0HHQ=="], - - "@solana/rpc-types/@solana/codecs-numbers": ["@solana/codecs-numbers@6.10.0", "", { "dependencies": { "@solana/codecs-core": "6.10.0", "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-CcM+wX4zOiA9zkh8A7t1787A0Ehgmu5+6Z2tKoHew6cNw/dkaUTPa8JnNHbvfsLC8dfHC1BhAEJl86sKmRsfkQ=="], - - "@solana/rpc-types/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/signers/@solana/codecs-core": ["@solana/codecs-core@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-nfAl9OMGo4HanIMxGsQoVB7BxMoqBCYEUxl8oEAZZ09pDxnaXQZkTRXEwPPccag37XfW1ciPd1vWPKwB2b0HHQ=="], - - "@solana/signers/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/subscribable/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/sysvars/@solana/codecs-core": ["@solana/codecs-core@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-nfAl9OMGo4HanIMxGsQoVB7BxMoqBCYEUxl8oEAZZ09pDxnaXQZkTRXEwPPccag37XfW1ciPd1vWPKwB2b0HHQ=="], - - "@solana/sysvars/@solana/codecs-numbers": ["@solana/codecs-numbers@6.10.0", "", { "dependencies": { "@solana/codecs-core": "6.10.0", "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-CcM+wX4zOiA9zkh8A7t1787A0Ehgmu5+6Z2tKoHew6cNw/dkaUTPa8JnNHbvfsLC8dfHC1BhAEJl86sKmRsfkQ=="], - - "@solana/sysvars/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/transaction-confirmation/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/transaction-messages/@solana/codecs-core": ["@solana/codecs-core@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-nfAl9OMGo4HanIMxGsQoVB7BxMoqBCYEUxl8oEAZZ09pDxnaXQZkTRXEwPPccag37XfW1ciPd1vWPKwB2b0HHQ=="], - - "@solana/transaction-messages/@solana/codecs-numbers": ["@solana/codecs-numbers@6.10.0", "", { "dependencies": { "@solana/codecs-core": "6.10.0", "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-CcM+wX4zOiA9zkh8A7t1787A0Ehgmu5+6Z2tKoHew6cNw/dkaUTPa8JnNHbvfsLC8dfHC1BhAEJl86sKmRsfkQ=="], - - "@solana/transaction-messages/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/transactions/@solana/codecs-core": ["@solana/codecs-core@6.10.0", "", { "dependencies": { "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-nfAl9OMGo4HanIMxGsQoVB7BxMoqBCYEUxl8oEAZZ09pDxnaXQZkTRXEwPPccag37XfW1ciPd1vWPKwB2b0HHQ=="], - - "@solana/transactions/@solana/codecs-numbers": ["@solana/codecs-numbers@6.10.0", "", { "dependencies": { "@solana/codecs-core": "6.10.0", "@solana/errors": "6.10.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-CcM+wX4zOiA9zkh8A7t1787A0Ehgmu5+6Z2tKoHew6cNw/dkaUTPa8JnNHbvfsLC8dfHC1BhAEJl86sKmRsfkQ=="], - - "@solana/transactions/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/wallet-standard-wallet-adapter-base/bs58": ["bs58@6.0.0", "", { "dependencies": { "base-x": "^5.0.0" } }, "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw=="], - - "@solana/web3.js/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - - "@solana/web3.js/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - "@streamdown/code/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], "@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], @@ -5897,11 +4598,9 @@ "@tanstack/router-plugin/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], - - "@types/connect/@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], + "@tanstack/router-utils/@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="], - "@types/graceful-fs/@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], "@types/jsonwebtoken/@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], @@ -5919,8 +4618,6 @@ "@vue/devtools-kit/hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], - "@wallet-standard/errors/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], - "aggregate-error/indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], "alchemy/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], @@ -5949,14 +4646,8 @@ "autumn-js/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "babel-jest/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "base-x/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "better-opn/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], - "body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], "boxen/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -5993,28 +4684,12 @@ "chevrotain/@chevrotain/types": ["@chevrotain/types@10.5.0", "", {}, "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A=="], - "chevrotain/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - - "chrome-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - - "chromium-edge-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "compression/negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], - - "compression/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "connect/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "connect/finalhandler": ["finalhandler@1.1.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "~2.3.0", "parseurl": "~1.3.3", "statuses": "~1.5.0", "unpipe": "~1.0.0" } }, "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA=="], - "crypto-random-string/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="], "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], @@ -6041,18 +4716,6 @@ "execa/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "expo/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - - "expo/react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], - - "expo-modules-autolinking/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "expo-modules-autolinking/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], - - "expo-router/query-string": ["query-string@7.1.3", "", { "dependencies": { "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" } }, "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg=="], - - "expo-router/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], - "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "extract-zip/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], @@ -6061,8 +4724,6 @@ "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "gray-matter/js-yaml": ["js-yaml@3.15.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag=="], "gray-matter/kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], @@ -6073,10 +4734,6 @@ "hast-util-raw/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], - "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], - - "hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "http-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "ink/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], @@ -6085,40 +4742,6 @@ "ink-text-input/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - "istanbul-lib-instrument/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "jayson/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], - - "jayson/@types/ws": ["@types/ws@7.4.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww=="], - - "jayson/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - - "jayson/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], - - "jest-haste-map/@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], - - "jest-message-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "jest-message-util/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - - "jest-mock/@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], - - "jest-util/@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], - - "jest-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "jest-util/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], - - "jest-util/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - - "jest-validate/camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], - - "jest-validate/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "jest-validate/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - - "jest-worker/@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], - "jsdom/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], "juice/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], @@ -6133,8 +4756,6 @@ "libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], - "lighthouse-logger/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "local-pkg/quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], "markdown-exit/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], @@ -6145,24 +4766,8 @@ "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], - "merge-options/is-plain-obj": ["is-plain-obj@2.1.0", "", {}, "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA=="], - "mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], - "metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="], - - "metro/hermes-parser": ["hermes-parser@0.35.0", "", { "dependencies": { "hermes-estree": "0.35.0" } }, "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA=="], - - "metro/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], - - "metro-babel-transformer/hermes-parser": ["hermes-parser@0.35.0", "", { "dependencies": { "hermes-estree": "0.35.0" } }, "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA=="], - - "metro-cache/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - - "metro-source-map/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], - - "metro-symbolicate/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], - "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "miniflare/sharp": ["sharp@0.35.2", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.4" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.2", "@img/sharp-darwin-x64": "0.35.2", "@img/sharp-freebsd-wasm32": "0.35.2", "@img/sharp-libvips-darwin-arm64": "1.3.1", "@img/sharp-libvips-darwin-x64": "1.3.1", "@img/sharp-libvips-linux-arm": "1.3.1", "@img/sharp-libvips-linux-arm64": "1.3.1", "@img/sharp-libvips-linux-ppc64": "1.3.1", "@img/sharp-libvips-linux-riscv64": "1.3.1", "@img/sharp-libvips-linux-s390x": "1.3.1", "@img/sharp-libvips-linux-x64": "1.3.1", "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", "@img/sharp-libvips-linuxmusl-x64": "1.3.1", "@img/sharp-linux-arm": "0.35.2", "@img/sharp-linux-arm64": "0.35.2", "@img/sharp-linux-ppc64": "0.35.2", "@img/sharp-linux-riscv64": "0.35.2", "@img/sharp-linux-s390x": "0.35.2", "@img/sharp-linux-x64": "0.35.2", "@img/sharp-linuxmusl-arm64": "0.35.2", "@img/sharp-linuxmusl-x64": "0.35.2", "@img/sharp-webcontainers-wasm32": "0.35.2", "@img/sharp-win32-arm64": "0.35.2", "@img/sharp-win32-ia32": "0.35.2", "@img/sharp-win32-x64": "0.35.2" } }, "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w=="], @@ -6173,16 +4778,8 @@ "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], - "mobile/expo-ui-ext": ["expo-ui-ext@file:apps/mobile/modules/expo-ui-ext", {}], - - "mobile/react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], - - "mobile/react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], - "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], - "node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], "octokit/@octokit/core": ["@octokit/core@5.2.2", "", { "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.0.0", "before-after-hook": "^2.2.0", "universal-user-agent": "^6.0.0" } }, "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg=="], @@ -6199,14 +4796,8 @@ "ora/cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], - "ox/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - - "ox/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], - "oxc-parser/@oxc-project/types": ["@oxc-project/types@0.142.0", "", {}, "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ=="], - "p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - "pac-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "pac-proxy-agent/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], @@ -6221,8 +4812,6 @@ "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], - "plist/@xmldom/xmldom": ["@xmldom/xmldom@0.9.10", "", {}, "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw=="], - "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], @@ -6237,28 +4826,10 @@ "proxy-agent/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], - "qrcode/pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], - - "qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], - - "react-native/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - - "react-native/react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], - - "react-native-svg/css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], - - "react-native-svg/css-tree": ["css-tree@1.1.3", "", { "dependencies": { "mdn-data": "2.0.14", "source-map": "^0.6.1" } }, "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q=="], - "rolldown-plugin-dts/get-tsconfig": ["get-tsconfig@5.0.0-beta.5", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ=="], "router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], - "rpc-websockets/@swc/helpers": ["@swc/helpers@0.5.21", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg=="], - - "rpc-websockets/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - - "rpc-websockets/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], - "run-jxa/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], "run-jxa/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], @@ -6267,38 +4838,26 @@ "sha.js/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "simple-plist/bplist-parser": ["bplist-parser@0.3.1", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA=="], - "sitemap/@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], "socks-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], - - "stacktrace-parser/type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="], - "strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "strip-literal/js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], "subsume/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], - "supports-hyperlinks/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], "svgo/css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], "svgo/css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], - "terminal-link/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], - "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - "test-exclude/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "to-regex-range/is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], "tsdown/rolldown": ["rolldown@1.2.1", "", { "dependencies": { "@oxc-project/types": "=0.142.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.2.1", "@rolldown/binding-darwin-arm64": "1.2.1", "@rolldown/binding-darwin-x64": "1.2.1", "@rolldown/binding-freebsd-x64": "1.2.1", "@rolldown/binding-linux-arm-gnueabihf": "1.2.1", "@rolldown/binding-linux-arm64-gnu": "1.2.1", "@rolldown/binding-linux-arm64-musl": "1.2.1", "@rolldown/binding-linux-ppc64-gnu": "1.2.1", "@rolldown/binding-linux-s390x-gnu": "1.2.1", "@rolldown/binding-linux-x64-gnu": "1.2.1", "@rolldown/binding-linux-x64-musl": "1.2.1", "@rolldown/binding-openharmony-arm64": "1.2.1", "@rolldown/binding-wasm32-wasi": "1.2.1", "@rolldown/binding-win32-arm64-msvc": "1.2.1", "@rolldown/binding-win32-x64-msvc": "1.2.1" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw=="], @@ -6311,12 +4870,6 @@ "unimport/magic-string": ["magic-string@1.1.0", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-kS3VHe0nEPST2saQV4Rbkchcd3UBRkVTQHo1D3h/ZTwFDhai/mfKkmtPAtD129EOI7K3HlHIsFOt0WrI2/oU9g=="], - "uniwind/@tailwindcss/node": ["@tailwindcss/node@4.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="], - - "uniwind/@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="], - - "uniwind/lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], - "unplugin-auto-import/magic-string": ["magic-string@1.1.0", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-kS3VHe0nEPST2saQV4Rbkchcd3UBRkVTQHo1D3h/ZTwFDhai/mfKkmtPAtD129EOI7K3HlHIsFOt0WrI2/oU9g=="], "unplugin-vue-components/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], @@ -6325,20 +4878,8 @@ "unstorage/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - "viem/@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="], - - "viem/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - - "viem/abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="], - - "viem/ox": ["ox@0.14.33", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-rooA/4o7bBof4Ge2VH/eovfNPb/AEEYyrNj03wggc55g5HZD8Pjs/OeWhttgjic3dDcqn0r29bDuvQEdTiUemQ=="], - - "viem/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], - "vite/rolldown": ["rolldown@1.2.1", "", { "dependencies": { "@oxc-project/types": "=0.142.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.2.1", "@rolldown/binding-darwin-arm64": "1.2.1", "@rolldown/binding-darwin-x64": "1.2.1", "@rolldown/binding-freebsd-x64": "1.2.1", "@rolldown/binding-linux-arm-gnueabihf": "1.2.1", "@rolldown/binding-linux-arm64-gnu": "1.2.1", "@rolldown/binding-linux-arm64-musl": "1.2.1", "@rolldown/binding-linux-ppc64-gnu": "1.2.1", "@rolldown/binding-linux-s390x-gnu": "1.2.1", "@rolldown/binding-linux-x64-gnu": "1.2.1", "@rolldown/binding-linux-x64-musl": "1.2.1", "@rolldown/binding-openharmony-arm64": "1.2.1", "@rolldown/binding-wasm32-wasi": "1.2.1", "@rolldown/binding-win32-arm64-msvc": "1.2.1", "@rolldown/binding-win32-x64-msvc": "1.2.1" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw=="], - "vue-router/@babel/generator": ["@babel/generator@8.0.0", "", { "dependencies": { "@babel/parser": "^8.0.0", "@babel/types": "^8.0.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "@types/jsesc": "^2.5.0", "jsesc": "^3.0.2" } }, "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g=="], - "vue-router/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "web-resource-inliner/htmlparser2": ["htmlparser2@9.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "entities": "^4.5.0" } }, "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ=="], @@ -6347,16 +4888,10 @@ "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "whatwg-url-without-unicode/webidl-conversions": ["webidl-conversions@5.0.0", "", {}, "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA=="], - "wrangler/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - "xcode/uuid": ["uuid@7.0.3", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg=="], - - "xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], - "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "@alchemy.run/cloudflare-runtime/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260704.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-XO+vvdhhTNZSsIWCkZ+JaE/JrFUmQAB0H0y/sVkAf32xa2TYBRvFUMwjSqf6WxtlxcKPQvmbLfpO/UQ0l/q4eQ=="], @@ -6375,11 +4910,9 @@ "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], - "@babel/highlight/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + "@babel/generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0", "", {}, "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg=="], - "@babel/highlight/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - - "@babel/highlight/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + "@babel/generator/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@8.0.4", "", {}, "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], @@ -6425,114 +4958,12 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], - "@expo/cli/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "@expo/cli/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - - "@expo/cli/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "@expo/cli/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "@expo/cli/ora/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], - - "@expo/cli/ora/cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="], - - "@expo/cli/ora/log-symbols": ["log-symbols@2.2.0", "", { "dependencies": { "chalk": "^2.0.1" } }, "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg=="], - - "@expo/cli/ora/strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="], - - "@expo/cli/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "@expo/cli/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - - "@expo/cli/send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "@expo/cli/send/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], - - "@expo/cli/send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - - "@expo/cli/send/range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - - "@expo/cli/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "@expo/cli/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "@expo/cli/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@expo/config-plugins/@expo/json-file/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], - - "@expo/config-plugins/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "@expo/config-plugins/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "@expo/devtools/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "@expo/devtools/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "@expo/env/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "@expo/env/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "@expo/fingerprint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "@expo/fingerprint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "@expo/image-utils/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "@expo/image-utils/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "@expo/local-build-cache-provider/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "@expo/local-build-cache-provider/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "@expo/metro-config/@expo/json-file/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], - - "@expo/metro-config/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "@expo/metro-config/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "@expo/metro-runtime/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "@expo/metro-runtime/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - - "@expo/package-manager/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "@expo/package-manager/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "@expo/package-manager/ora/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], - - "@expo/package-manager/ora/cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="], - - "@expo/package-manager/ora/log-symbols": ["log-symbols@2.2.0", "", { "dependencies": { "chalk": "^2.0.1" } }, "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg=="], - - "@expo/package-manager/ora/strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="], - - "@expo/xcpretty/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "@expo/xcpretty/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "@inquirer/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - - "@jest/environment/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - - "@jest/fake-timers/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - - "@jest/transform/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "@jest/transform/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "@jest/types/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - - "@jest/types/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "@jest/types/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "@maizzle/framework/oxfmt/@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.61.0", "", { "os": "android", "cpu": "arm" }, "sha512-BaS+1OVvg9sr+Xav0+KdWedQRcAzrdoEcwMZeqoc2F6ieC1s/t5eM35YQoRPQ7vAqkZ+p3tbQb1r9I9mrV5oGA=="], "@maizzle/framework/oxfmt/@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.61.0", "", { "os": "android", "cpu": "arm64" }, "sha512-of8atAV0M1egGcVOMbgZCvc10sFOP3ayQBNQV5h5G3fNq8gACdEswfFk9bzGrdbM23rtg0Coxi7np7oPLcueNw=="], @@ -6665,90 +5096,6 @@ "@opentelemetry/instrumentation-fetch/@opentelemetry/sdk-trace-web/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.1.0", "", { "dependencies": { "@opentelemetry/core": "2.1.0", "@opentelemetry/resources": "2.1.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-uTX9FBlVQm4S2gVQO1sb5qyBLq/FPjbp+tmGoxu4tIgtYGmBYB44+KX/725RFDe30yBSaA9Ml9fqphe1hbUyLQ=="], - "@react-native/community-cli-plugin/@react-native/dev-middleware/@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.83.4", "", {}, "sha512-mCE2s/S7SEjax3gZb6LFAraAI3x13gRVWJWqT0HIm71e4ITObENNTDuMw4mvZ/wr4Gz2wv4FcBH5/Nla9LXOcg=="], - - "@react-native/community-cli-plugin/@react-native/dev-middleware/@react-native/debugger-shell": ["@react-native/debugger-shell@0.83.4", "", { "dependencies": { "cross-spawn": "^7.0.6", "fb-dotslash": "0.5.8" } }, "sha512-FtAnrvXqy1xeZ+onwilvxEeeBsvBlhtfrHVIC2R/BOJAK9TbKEtFfjio0wsn3DQIm+UZq48DSa+p9jJZ2aJUww=="], - - "@react-native/community-cli-plugin/@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], - - "@react-native/community-cli-plugin/@react-native/dev-middleware/serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], - - "@react-native/dev-middleware/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], - - "@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - - "@react-native/dev-middleware/serve-static/send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], - - "@react-navigation/core/query-string/decode-uri-component": ["decode-uri-component@0.2.2", "", {}, "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="], - - "@react-navigation/core/query-string/filter-obj": ["filter-obj@1.1.0", "", {}, "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ=="], - - "@react-navigation/core/query-string/split-on-first": ["split-on-first@1.1.0", "", {}, "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="], - - "@solana/accounts/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/addresses/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/assertions/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/codecs-data-structures/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/codecs-strings/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/codecs/@solana/codecs-core/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/codecs/@solana/codecs-numbers/@solana/errors": ["@solana/errors@6.10.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "15.0.0" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"], "bin": { "errors": "bin/cli.mjs" } }, "sha512-KBLAxCtAXr357JNhCyIDQXWbuSj5vN6w+28FSfcYY6OOSiphmXLAV3V58jgV0C6iNbIzFJFi6yatFyDTdeJsNg=="], - - "@solana/fixed-points/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/instruction-plans/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/instructions/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/keys/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/kit/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/offchain-messages/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/options/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/program-client-core/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/programs/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/rpc-api/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/rpc-spec/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/rpc-subscriptions-channel-websocket/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/rpc-subscriptions-spec/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/rpc-subscriptions/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/rpc-transformers/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/rpc-transport-http/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/rpc-types/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/rpc/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/signers/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/subscribable/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/sysvars/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/transaction-confirmation/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/transaction-messages/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/transactions/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/wallet-standard-wallet-adapter-base/bs58/base-x": ["base-x@5.0.1", "", {}, "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg=="], - "@streamdown/code/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], "@streamdown/code/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], @@ -6829,10 +5176,6 @@ "@tanstack/router-plugin/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], - "@types/connect/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - - "@types/graceful-fs/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - "@types/jsonwebtoken/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "@types/sax/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], @@ -6913,16 +5256,6 @@ "astro/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "babel-jest/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "babel-jest/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "better-opn/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], - - "better-opn/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], - - "better-opn/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - "cheerio-select/css-select/nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], "cheerio-select/domutils/dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], @@ -6941,26 +5274,12 @@ "cheerio/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "chrome-launcher/is-wsl/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], - - "chromium-edge-launcher/is-wsl/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], - "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "connect/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "connect/finalhandler/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], - - "connect/finalhandler/on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="], - - "connect/finalhandler/statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="], - "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], @@ -7021,24 +5340,8 @@ "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "expo-modules-autolinking/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "expo-modules-autolinking/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "expo-router/query-string/decode-uri-component": ["decode-uri-component@0.2.2", "", {}, "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="], - - "expo-router/query-string/filter-obj": ["filter-obj@1.1.0", "", {}, "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ=="], - - "expo-router/query-string/split-on-first": ["split-on-first@1.1.0", "", {}, "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="], - - "expo/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "expo/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "glob/minimatch/brace-expansion": ["brace-expansion@1.1.18", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw=="], - "gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], "hast-util-from-html/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], @@ -7047,44 +5350,6 @@ "ink-confirm-input/ink-text-input/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], - "jayson/@types/ws/@types/node": ["@types/node@24.13.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], - - "jest-haste-map/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - - "jest-message-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "jest-message-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "jest-message-util/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "jest-message-util/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - - "jest-mock/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - - "jest-util/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - - "jest-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "jest-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "jest-validate/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "jest-validate/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "jest-validate/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "jest-validate/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - - "jest-worker/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - - "lighthouse-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "metro-babel-transformer/hermes-parser/hermes-estree": ["hermes-estree@0.35.0", "", {}, "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg=="], - - "metro-cache/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - - "metro/hermes-parser/hermes-estree": ["hermes-estree@0.35.0", "", {}, "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg=="], - "miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.1" }, "os": "darwin", "cpu": "arm64" }, "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg=="], "miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.1" }, "os": "darwin", "cpu": "x64" }, "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw=="], @@ -7133,10 +5398,6 @@ "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], - "node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], - - "node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - "octokit/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@4.0.0", "", {}, "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA=="], "octokit/@octokit/core/@octokit/graphql": ["@octokit/graphql@7.1.1", "", { "dependencies": { "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g=="], @@ -7161,32 +5422,6 @@ "parse5-parser-stream/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "qrcode/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], - - "qrcode/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "qrcode/yargs/y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], - - "qrcode/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], - - "react-native-svg/css-select/boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], - - "react-native-svg/css-select/css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], - - "react-native-svg/css-select/domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], - - "react-native-svg/css-select/domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], - - "react-native-svg/css-select/nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], - - "react-native-svg/css-tree/mdn-data": ["mdn-data@2.0.14", "", {}, "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="], - - "react-native-svg/css-tree/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - - "react-native/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "react-native/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "run-jxa/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], "run-jxa/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], @@ -7207,10 +5442,6 @@ "svgo/css-select/nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], - "terminal-link/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], - - "test-exclude/minimatch/brace-expansion": ["brace-expansion@1.1.18", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw=="], - "tsdown/rolldown/@oxc-project/types": ["@oxc-project/types@0.142.0", "", {}, "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ=="], "tsdown/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA=="], @@ -7293,64 +5524,12 @@ "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], - "uniwind/@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], - - "uniwind/@tailwindcss/node/tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], - - "uniwind/@tailwindcss/oxide/@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng=="], - - "uniwind/@tailwindcss/oxide/@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ=="], - - "uniwind/@tailwindcss/oxide/@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA=="], - - "uniwind/@tailwindcss/oxide/@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ=="], - - "uniwind/@tailwindcss/oxide/@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA=="], - - "uniwind/@tailwindcss/oxide/@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg=="], - - "uniwind/@tailwindcss/oxide/@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ=="], - - "uniwind/@tailwindcss/oxide/@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ=="], - - "uniwind/@tailwindcss/oxide/@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg=="], - - "uniwind/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.0", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA=="], - - "uniwind/@tailwindcss/oxide/@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ=="], - - "uniwind/@tailwindcss/oxide/@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="], - - "uniwind/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], - - "uniwind/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="], - - "uniwind/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="], - - "uniwind/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="], - - "uniwind/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="], - - "uniwind/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="], - - "uniwind/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="], - - "uniwind/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="], - - "uniwind/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="], - - "uniwind/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], - "unplugin-vue-components/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "unplugin-vue-markdown/markdown-exit/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], "unstorage/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], - "viem/ox/abitype": ["abitype@1.3.0", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-fk6Te+bojIFrMvMZrnOO+SxCB+RUksTGOzq/60ZRvs1L+BVzvi2bqt9L3W/17ZLdZsyM1FuYf65P5nlmoiH1Bg=="], - - "viem/ox/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], - "vite/rolldown/@oxc-project/types": ["@oxc-project/types@0.142.0", "", {}, "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ=="], "vite/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA=="], @@ -7383,10 +5562,6 @@ "vite/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ=="], - "vue-router/@babel/generator/@babel/parser": ["@babel/parser@8.0.4", "", { "dependencies": { "@babel/types": "^8.0.4" }, "bin": "./bin/babel-parser.js" }, "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g=="], - - "vue-router/@babel/generator/@babel/types": ["@babel/types@8.0.4", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0", "@babel/helper-validator-identifier": "^8.0.4" } }, "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g=="], - "vue-router/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "web-resource-inliner/htmlparser2/domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], @@ -7505,38 +5680,6 @@ "@astrojs/react/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "@babel/highlight/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - - "@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - - "@expo/cli/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - - "@expo/cli/ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], - - "@expo/cli/ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - - "@expo/cli/ora/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], - - "@expo/cli/ora/cli-cursor/restore-cursor": ["restore-cursor@2.0.0", "", { "dependencies": { "onetime": "^2.0.0", "signal-exit": "^3.0.2" } }, "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q=="], - - "@expo/cli/ora/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], - - "@expo/cli/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "@expo/cli/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "@expo/cli/wrap-ansi/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "@expo/package-manager/ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], - - "@expo/package-manager/ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - - "@expo/package-manager/ora/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], - - "@expo/package-manager/ora/cli-cursor/restore-cursor": ["restore-cursor@2.0.0", "", { "dependencies": { "onetime": "^2.0.0", "signal-exit": "^3.0.2" } }, "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q=="], - - "@expo/package-manager/ora/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], - "@inquirer/core/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "@inquirer/core/wrap-ansi/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -7561,24 +5704,6 @@ "@opentelemetry/instrumentation-fetch/@opentelemetry/sdk-trace-web/@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.1.0", "", { "dependencies": { "@opentelemetry/core": "2.1.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1CJjf3LCvoefUOgegxi8h6r4B/wLSzInyhGP2UmIBYNlo4Qk5CZ73e1eEyWmfXvFtm1ybkmfb2DqWvspsYLrWw=="], - "@react-native/community-cli-plugin/@react-native/dev-middleware/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], - - "@react-native/community-cli-plugin/@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - - "@react-native/community-cli-plugin/@react-native/dev-middleware/serve-static/send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], - - "@react-native/dev-middleware/serve-static/send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "@react-native/dev-middleware/serve-static/send/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], - - "@react-native/dev-middleware/serve-static/send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - - "@react-native/dev-middleware/serve-static/send/range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - - "@solana/codecs/@solana/codecs-core/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - - "@solana/codecs/@solana/codecs-numbers/@solana/errors/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - "astro/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], "astro/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], @@ -7633,14 +5758,10 @@ "cheerio-select/domutils/dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "ink-confirm-input/ink-text-input/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "ink-confirm-input/ink-text-input/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "jayson/@types/ws/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - "octokit/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@9.0.6", "", { "dependencies": { "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw=="], "octokit/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], @@ -7651,124 +5772,28 @@ "ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "qrcode/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "qrcode/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], - - "qrcode/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "qrcode/yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "qrcode/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "qrcode/yargs/yargs-parser/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], - - "react-native-svg/css-select/domhandler/domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], - - "react-native-svg/css-select/domutils/dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], - - "react-native-svg/css-select/domutils/domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], - "svgo/css-select/domhandler/domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], "svgo/css-select/domutils/dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "svgo/css-select/domutils/domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], - "test-exclude/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "tsdown/rolldown/@rolldown/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@2.0.0-alpha.3", "", { "dependencies": { "@emnapi/wasi-threads": "2.0.1", "tslib": "^2.4.0" } }, "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g=="], "tsdown/rolldown/@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@2.0.0-alpha.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA=="], - "uniwind/@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], - - "uniwind/@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], - - "uniwind/@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], - - "uniwind/@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], - - "uniwind/@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], - - "uniwind/@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], - - "uniwind/@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], - - "uniwind/@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], - - "uniwind/@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], - - "uniwind/@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], - - "uniwind/@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], - - "uniwind/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], - - "uniwind/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], - - "uniwind/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], - - "uniwind/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" }, "bundled": true }, "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw=="], - - "uniwind/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], - - "uniwind/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "vite/rolldown/@rolldown/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@2.0.0-alpha.3", "", { "dependencies": { "@emnapi/wasi-threads": "2.0.1", "tslib": "^2.4.0" } }, "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g=="], "vite/rolldown/@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@2.0.0-alpha.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA=="], - "vue-router/@babel/generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0", "", {}, "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg=="], - - "vue-router/@babel/generator/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@8.0.4", "", {}, "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg=="], - "web-resource-inliner/htmlparser2/domutils/dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], - "@babel/highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - - "@expo/cli/ora/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - - "@expo/cli/ora/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - - "@expo/cli/ora/cli-cursor/restore-cursor/onetime": ["onetime@2.0.1", "", { "dependencies": { "mimic-fn": "^1.0.0" } }, "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ=="], - - "@expo/package-manager/ora/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - - "@expo/package-manager/ora/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - - "@expo/package-manager/ora/cli-cursor/restore-cursor/onetime": ["onetime@2.0.1", "", { "dependencies": { "mimic-fn": "^1.0.0" } }, "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ=="], - "@octokit/app/@octokit/webhooks/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], - "@react-native/community-cli-plugin/@react-native/dev-middleware/serve-static/send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "@react-native/community-cli-plugin/@react-native/dev-middleware/serve-static/send/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], - - "@react-native/community-cli-plugin/@react-native/dev-middleware/serve-static/send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - - "@react-native/community-cli-plugin/@react-native/dev-middleware/serve-static/send/range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - - "@react-native/dev-middleware/serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "qrcode/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "react-native-svg/css-select/domutils/dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - "svgo/css-select/domutils/dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "tsdown/rolldown/@rolldown/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@2.0.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ=="], "vite/rolldown/@rolldown/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@2.0.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ=="], - - "@expo/cli/ora/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - - "@expo/cli/ora/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@1.2.0", "", {}, "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="], - - "@expo/package-manager/ora/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - - "@expo/package-manager/ora/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@1.2.0", "", {}, "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="], - - "@react-native/community-cli-plugin/@react-native/dev-middleware/serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], } } diff --git a/packages/db/package.json b/packages/db/package.json index 54eca16ce..20c6c3426 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -19,12 +19,14 @@ "db:ensure-privileges": "bun scripts/ensure-privileges.ts", "db:reset-preview": "bun scripts/reset-preview-branch.ts", "db:normalize-preview": "bun scripts/normalize-preview-ownership.ts", - "ps:apply-schema": "bun scripts/planetscale-apply-schema.ts" + "ps:apply-schema": "bun scripts/planetscale-apply-schema.ts", + "db:backfill:dashboards-v3": "bun scripts/backfill-dashboard-datasource-v3.ts" }, "dependencies": { "@electric-sql/pglite": "^0.5.2", "@maple/domain": "workspace:*", "drizzle-orm": "^0.45.1", + "effect": "catalog:effect", "postgres": "^3.4.9" }, "devDependencies": { @@ -33,6 +35,7 @@ "@types/node": "catalog:tooling", "drizzle-kit": "^0.31.9", "typescript": "catalog:tooling", - "vitest": "catalog:" + "vitest": "catalog:", + "@maple/widgets": "workspace:*" } } diff --git a/packages/db/scripts/backfill-dashboard-datasource-v3.ts b/packages/db/scripts/backfill-dashboard-datasource-v3.ts new file mode 100644 index 000000000..57eb5624b --- /dev/null +++ b/packages/db/scripts/backfill-dashboard-datasource-v3.ts @@ -0,0 +1,355 @@ +#!/usr/bin/env bun +/** + * One-shot backfill: rewrite every stored dashboard document's data sources from + * the v2 `{ endpoint, params }` bag to the v3 discriminated union. + * + * Runs ONCE per branch. There is no lazy read-path upgrade for v2 -> v3 — the + * deployed code decodes v3 only — so between deploying that code and finishing + * this run, un-backfilled dashboards fail to load. Keep the gap short. + * + * Usage: + * bun run db:backfill:dashboards-v3 --branch main # dry run (default) + * bun run db:backfill:dashboards-v3 --branch main --apply + * bun run db:backfill:dashboards-v3 --url postgres://… # local rehearsal + * bun run db:backfill:dashboards-v3 --url … --restore-from dump.jsonl + * + * Safety properties, each of which exists for a specific failure: + * + * - DRY RUN IS THE DEFAULT. `--apply` is the only thing that writes. + * - "Done" is decided STRUCTURALLY (`isDocumentV3`), never by a flag or a + * version column, so re-running is inherently a no-op and no lost cursor can + * cause a double transform. + * - Every row is DECODED as v3 after transform. A row that fails is left byte + * identical and reported; writing a document we could not decode is the one + * irreversible mistake available here. + * - The pre-write JSONL dump is flushed BEFORE the batch that it covers, so a + * crash mid-run still leaves every already-written row recoverable. + * - Writes CAS on `version`. Not bumping it would not avoid disturbing clients + * (the change streams over Electric either way) — it would let a stale tab + * win the compare-and-swap and silently overwrite the backfill with v2. + * Bumping turns that into the `DashboardConcurrencyError` every writer + * already retries. + * - `updated_at` is NOT touched: it is user-visible and the dashboard list's + * sort key (`dashboards_org_updated_idx`). A backfill that reshuffles every + * customer's list is a visible regression for zero benefit. + * - Keyset pagination on `(org_id, id)`, never OFFSET — rows move under you as + * you write. One small transaction per batch, not per run: a single large + * transaction holds locks against live writers and lands on Electric as one + * enormous change that forces every connected browser to resync at once. + * + * `dashboard_versions.snapshot_json` gets a second pass AFTER the dashboards + * pass, so a partial run still leaves live documents correct. Those snapshots are + * read through the same hard-failing `parsePayload`, so leaving them in v2 breaks + * the version-history page, not merely restore. They are not Electric-streamed, + * so they carry no open-tab risk. + */ +import { Option, Schema } from "effect" +import postgres from "postgres" +import { DashboardDocument, isDocumentV3, upgradeStoredDocument } from "@maple/widgets/dashboard" +import { fail, withBranchConnection } from "./planetscale-connection" + +const BATCH_DEFAULT = 100 +/** Breathing room for live traffic between batches. */ +const BATCH_PAUSE_MS = 150 + +// `Option`, not `Either`: this only ever asks "did it decode?", and the issue +// text comes from a second `decodeUnknownExit` on the failure path where it is +// actually wanted. +const decodeDocument = Schema.decodeUnknownOption(DashboardDocument) +const decodeIssue = (payload: unknown): string => { + const exit = Schema.decodeUnknownExit(DashboardDocument)(payload) + return exit._tag === "Failure" ? String(exit.cause) : "" +} + +interface Args { + readonly branch?: string + readonly url?: string + readonly apply: boolean + readonly batch: number + readonly dump: string + readonly quarantine: string + readonly restoreFrom?: string + readonly skipVersions: boolean +} + +const parseArgs = (argv: ReadonlyArray): Args => { + const value = (flag: string): string | undefined => { + const index = argv.indexOf(flag) + return index === -1 ? undefined : argv[index + 1] + } + const stamp = value("--stamp") ?? "run" + return { + branch: value("--branch"), + url: value("--url"), + apply: argv.includes("--apply"), + batch: Number(value("--batch") ?? BATCH_DEFAULT), + dump: value("--dump") ?? `dashboards-v3-backfill-${stamp}.jsonl`, + quarantine: value("--quarantine") ?? `dashboards-v3-quarantine-${stamp}.jsonl`, + restoreFrom: value("--restore-from"), + skipVersions: argv.includes("--skip-versions"), + } +} + +interface Report { + scanned: number + alreadyV3: number + converted: number + brokenBefore: number + quarantined: number + casMissed: number +} + +const emptyReport = (): Report => ({ + scanned: 0, + alreadyV3: 0, + converted: 0, + brokenBefore: 0, + quarantined: 0, + casMissed: 0, +}) + +interface Row { + readonly org_id: string + readonly id: string + readonly version: number + readonly payload_json: unknown +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) + +/** + * Classifies a row without writing anything. + * + * `brokenBefore` is reported separately from `quarantined` on purpose: a row that + * does not decode BEFORE the transform is already failing in production today + * (`parsePayload` hard-fails, so `get`/`list` already error for that org). Mixing + * the two would send someone hunting a regression that predates this change, and + * the dry run's `brokenBefore` count is the sharpest available pre-flight signal + * — it is exactly how many dashboards the flip strands. + */ +const classify = (payload: unknown) => { + if (isDocumentV3(payload)) return { kind: "already_v3" as const } + + const upgraded = upgradeStoredDocument(payload) + if (Option.isNone(decodeDocument(upgraded))) { + const issue = decodeIssue(upgraded) + // Did it decode BEFORE the transform? If not, this row is already broken in + // production and the flip is not what stranded it. + return Option.isNone(decodeDocument(payload)) + ? { kind: "broken_before" as const, issue } + : { kind: "quarantined" as const, issue } + } + return { kind: "converted" as const, upgraded } +} + +const backfillDashboards = async ( + sql: postgres.Sql, + args: Args, + dump: (line: unknown) => void, + quarantine: (line: unknown) => void, +): Promise => { + const report = emptyReport() + let cursor: { org: string; id: string } | null = null + + for (;;) { + const rows: Row[] = cursor + ? await sql` + SELECT org_id, id, version, payload_json FROM dashboards + WHERE (org_id, id) > (${cursor.org}, ${cursor.id}) + ORDER BY org_id, id LIMIT ${args.batch}` + : await sql` + SELECT org_id, id, version, payload_json FROM dashboards + ORDER BY org_id, id LIMIT ${args.batch}` + if (rows.length === 0) break + + const writes: Array<{ row: Row; upgraded: unknown }> = [] + for (const row of rows) { + report.scanned += 1 + const outcome = classify(row.payload_json) + if (outcome.kind === "already_v3") { + report.alreadyV3 += 1 + } else if (outcome.kind === "broken_before") { + report.brokenBefore += 1 + quarantine({ ...row, reason: "broken_before", issue: outcome.issue }) + } else if (outcome.kind === "quarantined") { + report.quarantined += 1 + quarantine({ ...row, reason: "quarantined", issue: outcome.issue }) + } else { + writes.push({ row, upgraded: outcome.upgraded }) + } + } + + if (args.apply && writes.length > 0) { + // Dump BEFORE the write, so a crash between the two still leaves every + // already-written row recoverable from the file. + for (const { row } of writes) dump(row) + + await sql.begin(async (tx) => { + for (const { row, upgraded } of writes) { + const result = await tx` + UPDATE dashboards + SET payload_json = ${tx.json(upgraded as never)}, version = version + 1 + WHERE org_id = ${row.org_id} AND id = ${row.id} AND version = ${row.version} + RETURNING id` + if (result.length === 0) report.casMissed += 1 + else report.converted += 1 + } + }) + } else { + report.converted += writes.length + } + + const last = rows[rows.length - 1]! + cursor = { org: last.org_id, id: last.id } + if (rows.length < args.batch) break + await sleep(BATCH_PAUSE_MS) + } + + return report +} + +const backfillVersionSnapshots = async ( + sql: postgres.Sql, + args: Args, + quarantine: (line: unknown) => void, +): Promise => { + const report = emptyReport() + let cursor: { org: string; id: string } | null = null + + for (;;) { + const rows: Array<{ org_id: string; id: string; snapshot_json: unknown }> = cursor + ? await sql` + SELECT org_id, id, snapshot_json FROM dashboard_versions + WHERE (org_id, id) > (${cursor.org}, ${cursor.id}) + ORDER BY org_id, id LIMIT ${args.batch}` + : await sql` + SELECT org_id, id, snapshot_json FROM dashboard_versions + ORDER BY org_id, id LIMIT ${args.batch}` + if (rows.length === 0) break + + for (const row of rows) { + report.scanned += 1 + const outcome = classify(row.snapshot_json) + if (outcome.kind === "already_v3") { + report.alreadyV3 += 1 + continue + } + if (outcome.kind !== "converted") { + // Downgraded to a report rather than a run-failing gate: a stranded + // historical snapshot degrades one entry in the version list. It does + // not lock anyone out of editing, which is what the live-document + // quarantine does. + report[outcome.kind === "broken_before" ? "brokenBefore" : "quarantined"] += 1 + quarantine({ ...row, reason: `snapshot_${outcome.kind}` }) + continue + } + report.converted += 1 + if (args.apply) { + // No CAS column and no user-visible timestamp here; `created_at` is left + // alone so history keeps its ordering. + await sql` + UPDATE dashboard_versions SET snapshot_json = ${sql.json(outcome.upgraded as never)} + WHERE org_id = ${row.org_id} AND id = ${row.id}` + } + } + + const last = rows[rows.length - 1]! + cursor = { org: last.org_id, id: last.id } + if (rows.length < args.batch) break + await sleep(BATCH_PAUSE_MS) + } + + return report +} + +/** + * Restores `payload_json` verbatim from a dump, guarded on `version`. + * + * A row someone has edited since the backfill fails the guard and is reported + * rather than reverted — reverting a user's later edit to undo our own write is + * strictly worse than leaving it. + */ +const restore = async (sql: postgres.Sql, path: string): Promise => { + const text = await Bun.file(path).text() + let restored = 0 + let skipped = 0 + for (const line of text.split("\n").filter((l) => l.trim().length > 0)) { + const row = JSON.parse(line) as Row + const result = await sql` + UPDATE dashboards SET payload_json = ${sql.json(row.payload_json as never)}, version = ${row.version} + WHERE org_id = ${row.org_id} AND id = ${row.id} AND version = ${row.version + 1} + RETURNING id` + if (result.length === 0) skipped += 1 + else restored += 1 + } + console.log(`\n✓ Restored ${restored} row(s); skipped ${skipped} edited since the backfill.`) +} + +const printReport = (label: string, report: Report, apply: boolean): void => { + console.log(`\n${label}${apply ? "" : " (DRY RUN — nothing written)"}`) + console.log(` scanned ${report.scanned}`) + console.log(` already v3 ${report.alreadyV3}`) + console.log(` ${apply ? "converted" : "would convert"} ${report.converted}`) + console.log(` broken before ${report.brokenBefore} (already failing in production today)`) + console.log(` quarantined ${report.quarantined} (left untouched)`) + if (apply) console.log(` CAS missed ${report.casMissed} (edited mid-run; re-run to pick up)`) +} + +const run = async (connectionUrl: string, args: Args): Promise => { + const sql = postgres(connectionUrl, { max: 1, prepare: false, onnotice: () => {} }) + const dumpLines: string[] = [] + const quarantineLines: string[] = [] + + try { + if (args.restoreFrom !== undefined) { + await restore(sql, args.restoreFrom) + return + } + + const dashboards = await backfillDashboards( + sql, + args, + (line) => dumpLines.push(JSON.stringify(line)), + (line) => quarantineLines.push(JSON.stringify(line)), + ) + printReport("dashboards", dashboards, args.apply) + + let snapshots: Report | null = null + if (!args.skipVersions) { + snapshots = await backfillVersionSnapshots(sql, args, (line) => + quarantineLines.push(JSON.stringify(line)), + ) + printReport("dashboard_versions", snapshots, args.apply) + } + + if (args.apply && dumpLines.length > 0) { + await Bun.write(args.dump, `${dumpLines.join("\n")}\n`) + console.log(`\n✓ Pre-write dump: ${args.dump} (${dumpLines.length} rows)`) + console.log(" Contains customer SQL and query definitions — treat as production data.") + } + if (quarantineLines.length > 0) { + await Bun.write(args.quarantine, `${quarantineLines.join("\n")}\n`) + console.log(`\n✗ Quarantine: ${args.quarantine} (${quarantineLines.length} rows)`) + } + + // Non-zero on a live-document quarantine only. `brokenBefore` is pre-existing + // breakage and must not make a clean run look failed — but it is reported, and + // it is the number that says how many dashboards the flip strands. + if (dashboards.quarantined > 0) { + fail(`${dashboards.quarantined} live dashboard(s) quarantined — inspect ${args.quarantine}`) + } + } finally { + await sql.end({ timeout: 5 }) + } +} + +const args = parseArgs(process.argv.slice(2)) +if (args.url === undefined && args.branch === undefined) { + fail("Pass --branch (PlanetScale) or --url (local rehearsal).") +} + +if (args.url !== undefined) { + await run(args.url, args) +} else { + await withBranchConnection(args.branch!, (url) => run(url, args)) +}