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/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/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 8ea3f7184..ec930a1a2 100644 --- a/apps/api/src/dashboard-templates/application/metric-overview.ts +++ b/apps/api/src/dashboard-templates/application/metric-overview.ts @@ -10,10 +10,12 @@ import { templateId, } 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 - metricType: string + metricType: QueryBuilderMetricType serviceName?: string aggregation?: string }): WidgetDef[] { @@ -28,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 }, }, @@ -154,7 +156,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/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/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/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/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/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/inspect-widget.ts b/apps/api/src/mcp/lib/inspect-widget.ts index bb4e0c37e..550b78d1c 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, @@ -35,11 +35,24 @@ import type { WidgetInspectionSummary, WidgetInspectionVerdict, } from "@maple/domain" +import { + dataSourceEndpoint, + dataSourceQuerySet, + dataSourceRawSql, + dataSourceTransform, + QUERY_SHAPE_ENDPOINTS, + RAW_SQL_ENDPOINT, +} 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" -const RAW_SQL_ENDPOINT = "raw_sql_chart" +// `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 // so a wide/long result doesn't bloat the response. @@ -49,7 +62,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) @@ -73,15 +90,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 +335,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 +347,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 +406,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", @@ -442,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") @@ -616,15 +623,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") { @@ -693,7 +696,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/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/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/__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/create-dashboard.ts b/apps/api/src/mcp/tools/create-dashboard.ts index 594d182b5..901045081 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, @@ -24,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) @@ -93,14 +100,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 +132,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 } @@ -157,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, } @@ -171,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/inspect-chart-data.ts b/apps/api/src/mcp/tools/inspect-chart-data.ts index f9dd409a5..6bb9880ee 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" @@ -69,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, @@ -82,21 +84,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 +280,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/apps/api/src/mcp/tools/query-data.ts b/apps/api/src/mcp/tools/query-data.ts index 7e677249c..007e6a654 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/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/v1/query-engine.http.ts b/apps/api/src/routes/v1/query-engine.http.ts index 5c36f1f6d..89f6329ca 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, + computeBucketSecondsForRange, formatWarehouseDateTime, parseWarehouseDateTime, QueryEngineExecuteBatchResponse, @@ -1789,24 +1790,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`, 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 - } - 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 computeBucketSecondsForRange(startTime, endTime, "rawSql") } 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/alerts/AlertRuleModel.ts b/apps/api/src/services/alerts/AlertRuleModel.ts index 4037b8eb3..8272db3c5 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, @@ -18,6 +19,7 @@ import { AlertSignalType as AlertSignalTypeSchema, AlertValidationError, QueryBuilderQueryDraftSchema, + UNGROUPED_GROUP_KEY, UserId, type AlertComparator, type AlertDestinationId, @@ -33,6 +35,7 @@ import type { AlertRuleRow } from "@maple/db" import { Array as Arr, Effect, 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) @@ -170,6 +173,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, @@ -198,7 +219,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 }), @@ -216,7 +237,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/alerts/AlertsService.ts b/apps/api/src/services/alerts/AlertsService.ts index 0e3929311..f88c09434 100644 --- a/apps/api/src/services/alerts/AlertsService.ts +++ b/apps/api/src/services/alerts/AlertsService.ts @@ -103,6 +103,7 @@ import { compileRulePlan, decodeStoredAlertRuleMetadata, isGroupedPlan, + toStorageGroupKey, makeAlertValidationError as makeValidationError, planEvaluateSource, type NormalizedRule, @@ -484,10 +485,9 @@ export class AlertsService extends Context.Service ({ evaluation: applyEvaluationLogic(rule, obs), - groupKey: grouped ? obs.groupKey : UNGROUPED_GROUP_KEY, + groupKey: toStorageGroupKey(plan, obs.groupKey), })) }) @@ -1058,10 +1058,9 @@ export class AlertsService extends Context.Service 0, 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 c4b732894..dbb75c849 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, makeRouteDataSource, makeStaticDataSource } from "@maple/widgets/dashboard" type UnknownRecord = Record type DashboardWidget = typeof DashboardWidgetSchema.Type @@ -278,29 +279,21 @@ 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"] { - return { endpoint: "markdown_static" } + return makeStaticDataSource() } function markdownWidgetContent(args: { diff --git a/apps/api/src/services/integrations/ScrapeTargetsService.ts b/apps/api/src/services/integrations/ScrapeTargetsService.ts index 754bebc88..633519dd9 100644 --- a/apps/api/src/services/integrations/ScrapeTargetsService.ts +++ b/apps/api/src/services/integrations/ScrapeTargetsService.ts @@ -1011,6 +1011,90 @@ 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 + }, + // An upstream failure is the scrape TARGET's fault, not ours — a + // persistence error here would report someone else's unreachable + // endpoint as a Maple storage fault, which is what the exact v2 + // error contracts (#458) set out to stop. + catch: (cause) => + toUpstreamError( + cause instanceof Error ? cause.message : "Scrape target request failed", + ), + }).pipe( + Effect.timeout(timeoutMs), + Effect.catchTag("TimeoutError", () => + Effect.fail(toUpstreamError("Scrape target request timed out")), + ), + ) + + 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, @@ -1065,37 +1149,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: (cause) => - toUpstreamError( - cause instanceof Error ? cause.message : "Scrape target request failed", - ), - }).pipe( - Effect.timeout(timeoutMs), - Effect.catchTag("TimeoutError", () => - Effect.fail(toUpstreamError("Scrape target request timed out")), - ), - ) + return yield* fetchUpstream(scrapeUrl, headers, row.value.targetType, timeoutMs) }) const recordScrapeResults = Effect.fn("ScrapeTargetsService.recordScrapeResults")(function* ( 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/package.json b/apps/web/package.json index b7ebfcfbe..0194e36b2 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -32,9 +32,11 @@ "@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:*", + "@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/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-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..b143385b0 100644 --- a/apps/web/src/api/warehouse/query-builder-breakdown.ts +++ b/apps/web/src/api/warehouse/query-builder-breakdown.ts @@ -1,9 +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 { 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}$/)) @@ -22,136 +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, - 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 -}) - -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 }) } @@ -163,33 +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] } }) -export const __testables = { - mergeBreakdownResults, -} +// 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.test.ts b/apps/web/src/api/warehouse/query-builder-timeseries.test.ts index f5984f066..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,408 +1,51 @@ -import { assert, describe, expect, it } from "@effect/vitest" -import { Effect } from "effect" -import type { QuerySpec } from "@maple/query-engine" +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" -function makeQueryResult(overrides: Partial = {}): QueryRunResult { - return { - queryId: "q-1", - queryName: "A", - source: "traces", - status: "success", - error: null, - warnings: [], - data: [], - ...overrides, - } -} - -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) - }) - - 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, - { +// 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", () => { + expect( + __testables.resolveStrategy({ + startTime: "2026-01-01 00:00:00", + endTime: "2026-01-01 01:00:00", + queries: [], + strategy: { enableEmptyRangeFallback: true, - fallbackWindowSeconds: [24 * 60 * 60, 7 * 24 * 60 * 60], - maxFallbackRangeSeconds: 31 * 24 * 60 * 60, + fallbackWindowSeconds: [7200, 60], + maxFallbackRangeSeconds: 86400, }, - 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("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) - }) - - 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 } }], - }), - ]) - - expect(count).toBe(1) - }) - - 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: [], + ).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: [], }), - ]) - - 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) - }) - - 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") + ).toEqual(LAB_EMPTY_RANGE_STRATEGY) }) }) diff --git a/apps/web/src/api/warehouse/query-builder-timeseries.ts b/apps/web/src/api/warehouse/query-builder-timeseries.ts index ed81f3440..0db725b3a 100644 --- a/apps/web/src/api/warehouse/query-builder-timeseries.ts +++ b/apps/web/src/api/warehouse/query-builder-timeseries.ts @@ -1,54 +1,32 @@ -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 { QueryBuilderQueryDraftSchema } from "@maple/domain/http" -import { buildTimeseriesQuerySpec } from "@maple/query-engine/query-builder" +import { QueryBuilderFormulaSchema, QueryComparisonSchema } from "@maple/query-model" import { - decodeInput, - executeQueryEngine, - invalidWarehouseInput, - type BackendError, - type WarehouseApiError, -} from "@/api/warehouse/effect-utils" -import { displayError } from "@/lib/error-messages" -import { computeBucketSeconds } from "@/api/warehouse/timeseries-utils" + LAB_EMPTY_RANGE_STRATEGY, + type EmptyRangeFallbackStrategy, + type TimeseriesQuerySetDiagnostics, + resolveFallbackStrategy, + runTimeseriesQuerySet, +} from "@maple/query-engine/query-set" +import { decodeInput, invalidWarehouseInput } from "@/api/warehouse/effect-utils" +import { makeWarehouseExecutor } from "@/api/warehouse/query-set-executor" -type ExecuteError = WarehouseApiError | BackendError +/** + * 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. 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. + */ 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,654 +39,50 @@ 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), }) 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: "none" | "previous_period" - 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 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 - } - - return { - ...spec, - bucketSeconds: computeAutoBucketSeconds(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 = computeAutoBucketSeconds(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, - )?.error - - 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, - ), - ) - - 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* ( - 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, - } - }, -) - -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}`, - } + /** + * 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 } -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, - 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, - } -}) - /** - * Ids of queries and formulas whose series must not be plotted. + * The wire strategy shape (`enableEmptyRangeFallback` / `fallbackWindowSeconds` / + * `maxFallbackRangeSeconds`) mapped onto the package's. * - * `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. + * 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 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), - })) +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, + ) } -export const __testables = { - computeAutoBucketSeconds, - resolveTimeseriesBucketSpec, - resolveExecutionSpecForWindow, - buildExecutionWindows, - resolveStrategy, - executeTimeseriesQueryWithFallbackUsing, - noQueryDataMessage, - countSuccessfulQuerySeries, - collectHiddenResultIds, - mergeQueryRunResults, - appendPercentChangeSeries, -} +const executor = makeWarehouseExecutor("queryEngine.timeseriesQuery") export function getQueryBuilderTimeseries({ data }: { data: QueryBuilderTimeseriesInput }) { return getQueryBuilderTimeseriesEffect({ data }) @@ -720,151 +94,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(executor, { + 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, - }, - strategy: { - enableEmptyRangeFallback: strategy.enableEmptyRangeFallback, - fallbackWindowSeconds: strategy.fallbackWindowSeconds, - maxFallbackRangeSeconds: strategy.maxFallbackRangeSeconds, - }, - 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/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/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/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/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 ca04aacfe..4270b702d 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() @@ -582,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 3e67c55f4..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,5 +1,6 @@ import * as React from "react" import { widgetTypeByVisualization } from "@maple/domain/http" +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" @@ -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 } @@ -107,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 @@ -117,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({ @@ -163,7 +154,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..981c96840 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,55 @@ 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 }), + ...(querySet.defaultLimit === undefined ? {} : { defaultLimit: querySet.defaultLimit }), + ...(querySet.limit === undefined ? {} : { limit: querySet.limit }), + ...(querySet.columns === undefined ? {} : { columns: querySet.columns }), + }, + } + } + + 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-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/list/dashboard-summary.ts b/apps/web/src/components/dashboard-builder/list/dashboard-summary.ts index c6a23eadd..98cbedd7f 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,12 +47,27 @@ 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", +} + +/** + * 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. */ @@ -55,11 +76,35 @@ 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 && querySet.queries.length > 0) { + const domains: DashboardDomain[] = [] + for (const query of querySet.queries) { + const domain = DRAFT_SOURCE_DOMAIN[query?.dataSource] + if (domain) domains.push(domain) + } + 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) + if (endpoint !== null) { + const domain = ENDPOINT_DOMAIN[endpoint] + if (domain) return [domain] + } + return querySet === null ? [] : [DEFAULT_QUERY_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.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 3d91cb1a1..0ae5277cc 100644 --- a/apps/web/src/components/dashboard-builder/portable-dashboard.ts +++ b/apps/web/src/components/dashboard-builder/portable-dashboard.ts @@ -81,11 +81,20 @@ export function toPortableDashboard(dashboard: Dashboard): PortableDashboard { } } +// 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 15f3391c9..c48e3a093 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 } @@ -51,11 +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. -export type WidgetDataSource = Omit, "endpoint"> & { - endpoint: DataSourceEndpoint -} +// A straight alias of the schema type, as of v3. +// +// 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/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..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,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 { makeStaticDataSource } 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: () => makeStaticDataSource(), 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 2f2dbca5e..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 ? () => { @@ -117,11 +115,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/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 c260f1dd5..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,6 +15,7 @@ import type { WidgetDataSource, WidgetDisplayConfig, } from "@/components/dashboard-builder/types" +import { makeQueryDataSource, makeRouteDataSource, makeStaticDataSource } 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: makeStaticDataSource(), 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/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/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/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/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-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-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 d73623d92..4d5309906 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" @@ -6,20 +7,21 @@ 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" /** - * 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" @@ -82,7 +84,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 +127,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 @@ -387,6 +392,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 +414,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 +434,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 +459,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 +472,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 +480,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 +490,7 @@ export function useWidgetDataSource( if ( disableReason !== null || isStatic || - !dataSource || + request === null || !enabled || waitingOnVariables || (exceedsListCap && !narrowed) @@ -485,13 +498,13 @@ export function useWidgetDataSource( return disabledResultAtom() } return widgetFetchAtom({ - endpoint: dataSource.endpoint, + endpoint: request.endpoint, params: resolvedParams, }) }, [ disableReason, isStatic, - dataSource, + request, resolvedParams, enabled, waitingOnVariables, @@ -501,7 +514,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 9d252ada5..e9d629828 100644 --- a/apps/web/src/lib/alerts/widget-prefill.ts +++ b/apps/web/src/lib/alerts/widget-prefill.ts @@ -2,6 +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 { SERIES_REDUCER_TO_ALERT_REDUCER, toQueryBuilderDataSource } from "@maple/query-model" +import { dataSourceQuerySet, dataSourceRawSql, dataSourceTransform } from "@maple/widgets/dashboard" export type WidgetAlertPrefillNotice = { severity: "warning" | "error" @@ -29,12 +31,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) : {} } @@ -45,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 { @@ -71,14 +63,13 @@ 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 } -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 +95,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", @@ -132,19 +126,33 @@ 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, } } - 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 +179,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 +187,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/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-shared.ts b/apps/web/src/lib/query-builder/widget-builder-shared.ts index 81179411a..2b31f61fb 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,8 @@ 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 { QueryComparisonMode } from "@maple/query-model" import type { HeatmapColorScale, HeatmapScaleType } from "@maple/domain/http" import { normalizeKey, parseBoolean, parseWhereClause as parseWhereClauses } from "@maple/domain/where-clause" @@ -27,7 +29,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 @@ -43,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 @@ -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 { @@ -313,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.test.ts b/apps/web/src/lib/query-builder/widget-builder-utils.test.ts index c45d6d3a9..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 }, @@ -38,7 +57,6 @@ function makeState(): QueryBuilderWidgetState { formulas: [], comparisonMode: "none", includePercentChange: true, - debug: false, statAggregate: "first", statValueField: "", unit: "number", @@ -80,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"]) }) @@ -197,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, }) @@ -211,19 +229,19 @@ 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", () => { // 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 = { @@ -232,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", () => { @@ -243,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() } }) }) @@ -269,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", () => { @@ -337,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-builder-utils.ts b/apps/web/src/lib/query-builder/widget-builder-utils.ts index a5f51c838..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,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, makeQueryDataSource } 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,12 @@ export function toInitialState(widget: DashboardWidget): QueryBuilderWidgetState typeof rawComparison.includePercentChange === "boolean" ? rawComparison.includePercentChange : true, - debug: params.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 +130,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 @@ -156,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..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 @@ -59,7 +69,6 @@ function makeState(overrides: Partial = {}): QueryBuild formulas: [], comparisonMode: "none", includePercentChange: true, - debug: false, statAggregate: "first", statValueField: "", unit: "number", @@ -86,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, @@ -164,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() } }) @@ -176,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", () => { @@ -207,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/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/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/bun.lock b/bun.lock index a9e122908..cae3711bb 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", @@ -187,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": { @@ -268,9 +220,11 @@ "@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:*", + "@maple/widgets": "workspace:*", "@rrweb/replay": "^2.0.1", "@rrweb/types": "^2.0.1", "@streamdown/cjk": "^1.0.3", @@ -574,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", @@ -591,6 +547,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 +617,7 @@ "@maple-dev/clickhouse-builder": "workspace:*", "@maple/cache": "workspace:*", "@maple/domain": "workspace:*", + "@maple/query-model": "workspace:*", "effect": "catalog:effect", }, "devDependencies": { @@ -684,6 +642,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 +694,7 @@ "name": "@maple/widgets", "dependencies": { "@maple/primitives": "workspace:*", + "@maple/query-model": "workspace:*", "effect": "catalog:effect", }, "devDependencies": { @@ -782,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=="], @@ -872,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=="], @@ -1050,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=="], @@ -1076,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=="], @@ -1108,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=="], @@ -1178,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=="], @@ -1252,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=="], @@ -1468,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=="], @@ -1498,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=="], @@ -1592,6 +1324,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"], @@ -1654,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=="], @@ -1728,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=="], @@ -1972,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=="], @@ -2146,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=="], @@ -2172,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=="], @@ -2210,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=="], @@ -2342,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=="], @@ -2474,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=="], @@ -2550,17 +2068,7 @@ "@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/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="], "@types/jsesc": ["@types/jsesc@2.5.1", "", {}, "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw=="], @@ -2596,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=="], @@ -2608,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=="], @@ -2720,20 +2222,6 @@ "@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=="], - - "@wallet-standard/base": ["@wallet-standard/base@1.1.1", "", {}, "sha512-gggIHTtxicF9XFMQ12DkfS6NAG92Ak795JeSA7f2whAQ6Y3AkMWWuCMxSZXG2NIPN42kEaZSNVjqMsJRaJRxMQ=="], - - "@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=="], - - "@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=="], - - "@wallet-standard/features": ["@wallet-standard/features@1.1.1", "", { "dependencies": { "@wallet-standard/base": "^1.1.1" } }, "sha512-aCWYmVeSCGViyEU5k7GMoW8zxE4Gs+C1s1Pp2XLesvSNlnZ4PMES9HUnTB3hl0b3RVj7C61yze3IWyrncqg4MA=="], - - "@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=="], @@ -2790,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=="], @@ -2808,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=="], @@ -2820,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=="], @@ -2858,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=="], @@ -2896,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=="], @@ -2910,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=="], @@ -2938,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=="], @@ -3048,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=="], @@ -3082,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=="], @@ -3094,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=="], @@ -3114,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=="], @@ -3142,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=="], @@ -3156,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=="], @@ -3258,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=="], @@ -3270,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=="], @@ -3290,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=="], @@ -3304,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=="], @@ -3318,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=="], @@ -3380,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=="], @@ -3398,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=="], @@ -3414,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=="], @@ -3440,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=="], @@ -3452,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=="], @@ -3520,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=="], @@ -3532,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=="], @@ -3550,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=="], @@ -3574,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=="], @@ -3602,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=="], @@ -3620,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=="], @@ -3634,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=="], @@ -3702,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=="], @@ -3744,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=="], @@ -3766,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=="], @@ -3800,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=="], @@ -3816,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=="], @@ -3876,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=="], @@ -3916,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=="], @@ -3928,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=="], @@ -3958,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=="], @@ -3974,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=="], @@ -4006,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=="], @@ -4028,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=="], @@ -4052,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=="], @@ -4064,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=="], @@ -4116,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=="], @@ -4248,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=="], @@ -4278,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=="], @@ -4300,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=="], @@ -4322,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=="], @@ -4350,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=="], @@ -4366,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=="], @@ -4382,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=="], @@ -4404,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=="], @@ -4420,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=="], @@ -4464,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=="], @@ -4512,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=="], @@ -4522,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=="], @@ -4556,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=="], @@ -4592,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=="], @@ -4652,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=="], @@ -4668,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=="], @@ -4720,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=="], @@ -4750,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=="], @@ -4766,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=="], @@ -4804,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=="], @@ -4848,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=="], @@ -4898,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=="], @@ -4914,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=="], @@ -4964,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=="], @@ -4984,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=="], @@ -5014,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=="], @@ -5050,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=="], @@ -5084,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=="], @@ -5114,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=="], @@ -5156,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=="], @@ -5176,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=="], @@ -5212,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=="], @@ -5224,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=="], @@ -5238,13 +4248,7 @@ "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-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=="], @@ -5256,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=="], @@ -5288,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=="], @@ -5372,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=="], @@ -5426,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=="], @@ -5526,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=="], @@ -5584,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=="], @@ -5648,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=="], @@ -5698,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=="], @@ -5876,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=="], @@ -5898,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=="], @@ -5928,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=="], @@ -5972,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=="], @@ -6020,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=="], @@ -6040,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=="], @@ -6052,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=="], @@ -6064,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=="], @@ -6112,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=="], @@ -6124,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=="], @@ -6152,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=="], @@ -6178,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=="], @@ -6200,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=="], @@ -6216,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=="], @@ -6246,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=="], @@ -6290,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=="], @@ -6304,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=="], @@ -6326,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=="], @@ -6354,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=="], @@ -6404,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=="], @@ -6644,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=="], @@ -6808,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=="], @@ -6892,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=="], @@ -6920,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=="], @@ -7000,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=="], @@ -7026,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=="], @@ -7112,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=="], @@ -7140,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=="], @@ -7186,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=="], @@ -7272,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=="], @@ -7362,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=="], @@ -7484,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=="], @@ -7540,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=="], @@ -7612,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=="], @@ -7630,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)) +} diff --git a/packages/domain/package.json b/packages/domain/package.json index 0cae7e456..2ffb0161e 100644 --- a/packages/domain/package.json +++ b/packages/domain/package.json @@ -38,6 +38,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/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/dashboards.ts b/packages/domain/src/http/dashboards.ts index b2e052c4a..719cbe373 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/http/query-engine.ts b/packages/domain/src/http/query-engine.ts index 6a9490ed3..980efb671 100644 --- a/packages/domain/src/http/query-engine.ts +++ b/packages/domain/src/http/query-engine.ts @@ -1560,66 +1560,22 @@ export class WorkloadInfraTimeseriesResponse extends Schema.Class + QueryBuilderAddOnsSchema, + QueryBuilderFormulaSchema, + type QueryBuilderFormulaPayload, + type QueryBuilderQueryDraftPayload, + QueryBuilderQueryDraftSchema, + TracesQueryDraftSchema, +} from "@maple/query-model" // Raw SQL chart (Hyperdx-style — user-authored ClickHouse SQL with macros) 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/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/package.json b/packages/query-engine/package.json index f7bfb3698..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", @@ -30,6 +31,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/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/query-engine/src/datetime.test.ts b/packages/query-engine/src/datetime.test.ts index 24275b560..1134fe19c 100644 --- a/packages/query-engine/src/datetime.test.ts +++ b/packages/query-engine/src/datetime.test.ts @@ -1,8 +1,11 @@ import { describe, expect, it } from "vitest" import { bucketTimeline, + BUCKET_POLICIES, cacheSnapSecondsForRange, + alertWindowBucketSeconds, computeBucketSeconds, + computeBucketSecondsForRange, formatWarehouseDateTime, formatWarehouseDateTimeMs, parseWarehouseDateTime, @@ -107,6 +110,100 @@ 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("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) + 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..105ef7159 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,103 @@ 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) + // `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] } 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. + * + * 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/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-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-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/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/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/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/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 new file mode 100644 index 000000000..f66cc73b1 --- /dev/null +++ b/packages/query-engine/src/query-set/index.ts @@ -0,0 +1,32 @@ +// @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" +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" +export * from "./window" 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 +}) 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/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/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 +}) diff --git a/packages/query-engine/src/runtime/query-engine.ts b/packages/query-engine/src/runtime/query-engine.ts index 4e6e22e55..9bb428d47 100644 --- a/packages/query-engine/src/runtime/query-engine.ts +++ b/packages/query-engine/src/runtime/query-engine.ts @@ -29,7 +29,14 @@ 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, + BUCKET_POLICIES, + computeBucketSeconds, + formatWarehouseDateTime, + parseWarehouseDateTime, +} from "../datetime" +import { ENGINE_UNGROUPED_GROUP_KEY } from "../group-key" import { MAX_BREAKDOWN_RANGE_SECONDS, MAX_LIST_RANGE_SECONDS, @@ -60,7 +67,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 @@ -219,6 +226,11 @@ export const msToTinybirdDateTime = (ms: number): string => { const CACHE_SNAP_S = 15 const TRACE_SERVICE_PARTITION_BUFFER_MS = 24 * 60 * 60 * 1000 +// 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 * rows already selected for this page. A one-day cushion covers traces that @@ -701,9 +713,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) @@ -742,7 +754,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, { @@ -775,7 +787,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 @@ -1144,7 +1156,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, @@ -1993,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 || "") @@ -2002,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 ") } @@ -2106,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, }) @@ -2125,7 +2137,7 @@ export const computeAlertBuckets = Effect.fnUntraced(function* 0 ? sampleCount : null, sampleCount, }) @@ -2180,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, @@ -2215,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", @@ -2278,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) } @@ -2314,8 +2331,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 @@ -2330,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) => 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..415f4e20c --- /dev/null +++ b/packages/query-model/src/query-draft.ts @@ -0,0 +1,97 @@ +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] + +/** + * 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, + 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..f280732c9 --- /dev/null +++ b/packages/query-model/src/query-model.test.ts @@ -0,0 +1,173 @@ +import { Schema } from "effect" +import { describe, expect, it } from "vitest" +import { + ALERT_REDUCER_TO_SERIES_REDUCER, + ALERT_REDUCERS, + QuerySetSchema, + QueryBuilderQueryDraftSchema, + SERIES_REDUCER_TO_ALERT_REDUCER, + 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) + }) + + 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", () => { + 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..7f2f936c1 --- /dev/null +++ b/packages/query-model/src/result-shape.ts @@ -0,0 +1,10 @@ +/** + * 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] diff --git a/packages/query-model/src/series-reducer.ts b/packages/query-model/src/series-reducer.ts new file mode 100644 index 000000000..0b0e0a971 --- /dev/null +++ b/packages/query-model/src/series-reducer.ts @@ -0,0 +1,55 @@ +/** + * 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. + */ +/** + * 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" }, + // 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"] +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 + +/** + * 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/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/access.test.ts b/packages/widgets/src/dashboard/access.test.ts new file mode 100644 index 000000000..9e7d794d7 --- /dev/null +++ b/packages/widgets/src/dashboard/access.test.ts @@ -0,0 +1,171 @@ +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() + }) + + 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", () => { + 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..57726ffeb --- /dev/null +++ b/packages/widgets/src/dashboard/access.ts @@ -0,0 +1,171 @@ +import type { QuerySet, QueryResultShape } from "@maple/query-model" +import { QUERY_ENDPOINT_SHAPES, RAW_SQL_ENDPOINT } from "./legacy-endpoints" +import type { WidgetDataSourceTransformV2 } from "./shared/transform" + +/** + * 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 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. + * + * 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 +} + +/** + * 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 + +/** + * 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 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 + + 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 + return { resultShape, fields: isRecord(dataSource.params) ? dataSource.params : {} } + })() + if (source === null) return null + + const { resultShape, fields } = source + return { + resultShape, + 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, + } +} + +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 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") { + 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..e0eaea231 --- /dev/null +++ b/packages/widgets/src/dashboard/construct.test.ts @@ -0,0 +1,105 @@ +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. + expect(makeQueryDataSource({ resultShape: "list", queries: [] })).toEqual({ + kind: "query", + resultShape: "list", + queries: [], + }) + }) + + // 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", () => { + 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({ + kind: "route", + 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..1da82d532 --- /dev/null +++ b/packages/widgets/src/dashboard/construct.ts @@ -0,0 +1,128 @@ +import type { QueryResultShape, QuerySet } from "@maple/query-model" +import type { RawSqlDataSource } from "./access" +import type { WidgetDataSourceTransformV2 } from "./shared/transform" +import type { + QueryWidgetDataSource, + RawSqlWidgetDataSource, + RouteWidgetDataSource, + StaticWidgetDataSource, +} from "./v3/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. + * + * 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 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. + * + * `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 }, +): 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 }), +}) + +export interface RawSqlDataSourceInput extends RawSqlDataSource { + readonly transform?: WidgetDataSourceTransform +} + +/** A widget backed by user-authored ClickHouse SQL. */ +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 }), +}) + +/** + * 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. + * + * `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, +): 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 4b9bcb7ff..482d0bf0b 100644 --- a/packages/widgets/src/dashboard/index.ts +++ b/packages/widgets/src/dashboard/index.ts @@ -19,6 +19,50 @@ export { detectSchemaVersion, migrateToLatest, } from "./migrations" +export { + dataSourceEndpoint, + dataSourceQuerySet, + dataSourceRawSql, + dataSourceRouteParams, + dataSourceTransform, + isQueryDataSource, + QUERY_SHAPE_ENDPOINTS, + RAW_SQL_ENDPOINT, + type RawSqlDataSource, + type WidgetQuerySet, +} from "./access" +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" @@ -58,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 4a414ddb7..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 => { @@ -32,6 +35,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 = document.schemaVersion + if (typeof declared === "number" && declared > CURRENT_DASHBOARD_SCHEMA_VERSION) { + return document + } + let current: Record = document let version = detectSchemaVersion(document) @@ -44,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 c16c39e0c..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") }) @@ -222,25 +241,60 @@ 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) + }) + + // `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", () => { @@ -248,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/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" 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 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