@hachej/boring-bi-dashboard 0.1.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ import { B as BslDashboardSpec } from '../types-BGZKL9Rs.js';
2
+ export { e as BslChartRenderer, a as BslChartSpec, f as BslChartType, b as BslDashboardComponentSpec, c as BslDashboardQuerySpec, g as BslFilterControlSpec, h as BslMetricSpec, d as BslPerspectiveViewerSpec, i as BslTextSpec, D as DashboardGridSpec, P as PerspectiveViewerPlugin } from '../types-BGZKL9Rs.js';
3
+ import '@hachej/boring-generated-pane/shared';
4
+ import 'zod';
5
+
6
+ interface DashboardValidationResult {
7
+ spec: BslDashboardSpec | null;
8
+ errors: string[];
9
+ }
10
+ declare function parseDashboardSpec(value: unknown): DashboardValidationResult;
11
+ declare function validateDashboardSpec(value: unknown): {
12
+ ok: boolean;
13
+ errors: string[];
14
+ };
15
+
16
+ export { BslDashboardSpec, type DashboardValidationResult, parseDashboardSpec, validateDashboardSpec };
@@ -0,0 +1,131 @@
1
+ // src/shared/validation.ts
2
+ import { parseGeneratedPaneSpec } from "@hachej/boring-generated-pane/shared";
3
+
4
+ // src/shared/schemas.ts
5
+ import { z } from "zod";
6
+ var chartRenderers = ["echarts", "vega-lite", "plotly"];
7
+ var chartTypes = ["bar", "line", "area", "scatter", "heatmap", "pie", "treemap", "sunburst", "gauge", "table"];
8
+ var perspectivePlugins = ["Datagrid", "Y Bar", "X Bar", "Y Line", "Y Area", "Y Scatter", "Y Treemap", "Sunburst", "Heatmap"];
9
+ var metricFormats = ["number", "currency", "percent"];
10
+ var filterControlTypes = ["select", "multiSelect", "dateRange", "numberRange", "search"];
11
+ var sortDirections = ["asc", "desc"];
12
+ var dashboardQuerySchema = z.union([
13
+ z.object({
14
+ id: z.string().min(1),
15
+ model: z.string().min(1),
16
+ query: z.string().min(1),
17
+ limit: z.number().int().safe().min(1).optional()
18
+ }),
19
+ z.object({
20
+ id: z.string().min(1),
21
+ source: z.string().min(1).optional(),
22
+ sql: z.string().min(1),
23
+ params: z.record(z.string(), z.unknown()).optional(),
24
+ limit: z.number().int().safe().min(1).optional()
25
+ })
26
+ ]);
27
+ var dashboardGridPropsSchema = z.object({
28
+ title: z.string().optional(),
29
+ columns: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4), z.literal(6), z.literal(12)]).optional()
30
+ });
31
+ var bslMetricPropsSchema = z.object({
32
+ queryId: z.string(),
33
+ valueField: z.string(),
34
+ label: z.string(),
35
+ format: z.enum(metricFormats).optional()
36
+ });
37
+ var bslChartPropsSchema = z.object({
38
+ queryId: z.string(),
39
+ title: z.string().optional(),
40
+ renderer: z.enum(chartRenderers).optional(),
41
+ chartType: z.enum(chartTypes),
42
+ x: z.string().optional(),
43
+ y: z.union([z.string(), z.array(z.string())]).optional(),
44
+ color: z.string().optional(),
45
+ controls: z.array(z.string()).optional()
46
+ });
47
+ var bslPerspectiveViewerPropsSchema = z.object({
48
+ queryId: z.string(),
49
+ title: z.string().optional(),
50
+ plugin: z.enum(perspectivePlugins).optional(),
51
+ columns: z.array(z.string()).optional(),
52
+ groupBy: z.array(z.string()).optional(),
53
+ splitBy: z.array(z.string()).optional(),
54
+ sort: z.array(z.tuple([z.string(), z.enum(sortDirections)])).optional()
55
+ });
56
+ var bslFilterPropsSchema = z.object({
57
+ id: z.string(),
58
+ field: z.string(),
59
+ label: z.string().optional(),
60
+ controlType: z.enum(filterControlTypes),
61
+ targetQueries: z.array(z.string())
62
+ });
63
+ var bslTextPropsSchema = z.object({
64
+ markdown: z.string()
65
+ });
66
+ var componentPropsSchemas = {
67
+ DashboardGrid: dashboardGridPropsSchema,
68
+ BSLMetric: bslMetricPropsSchema,
69
+ BSLChart: bslChartPropsSchema,
70
+ BSLPerspectiveViewer: bslPerspectiveViewerPropsSchema,
71
+ BSLFilter: bslFilterPropsSchema,
72
+ BSLText: bslTextPropsSchema
73
+ };
74
+
75
+ // src/shared/validation.ts
76
+ function isRecord(value) {
77
+ return typeof value === "object" && value !== null && !Array.isArray(value);
78
+ }
79
+ function formatSchemaErrors(prefix, error) {
80
+ return error.issues.map((issue) => `${prefix}${issue.path.length ? `.${issue.path.map(String).join(".")}` : ""}: ${issue.message}`);
81
+ }
82
+ function parseDashboardSpec(value) {
83
+ const base = parseGeneratedPaneSpec(value);
84
+ if (!base.spec) return { spec: null, errors: base.errors };
85
+ const errors = [];
86
+ if (base.spec.profile !== "bi-dashboard") errors.push('dashboard spec profile must be "bi-dashboard"');
87
+ if (typeof base.spec.title !== "string" || base.spec.title.length === 0) errors.push("dashboard spec needs a title");
88
+ if (!isRecord(base.spec.queries)) errors.push("dashboard spec needs a queries object");
89
+ if (errors.length > 0) return { spec: null, errors };
90
+ const queries = base.spec.queries;
91
+ for (const [id, query] of Object.entries(queries)) {
92
+ const parsed = dashboardQuerySchema.safeParse(query);
93
+ if (!parsed.success) {
94
+ errors.push(...formatSchemaErrors(`query ${id}`, parsed.error));
95
+ continue;
96
+ }
97
+ if (parsed.data.id !== id) errors.push(`query ${id} must repeat its id field`);
98
+ }
99
+ for (const [id, element] of Object.entries(base.spec.elements)) {
100
+ const type = element.type;
101
+ const schema = componentPropsSchemas[type];
102
+ if (!schema) {
103
+ errors.push(`component ${id} has unsupported type ${String(element.type)}`);
104
+ continue;
105
+ }
106
+ const props = schema.safeParse(element.props ?? {});
107
+ if (!props.success) errors.push(...formatSchemaErrors(`component ${id}.props`, props.error));
108
+ if (type === "DashboardGrid" && !Array.isArray(element.children)) {
109
+ errors.push(`DashboardGrid ${id} must include string children`);
110
+ }
111
+ if ((type === "BSLMetric" || type === "BSLChart" || type === "BSLPerspectiveViewer") && props.success) {
112
+ const queryId = props.data.queryId;
113
+ if (!queries[queryId]) errors.push(`component ${id} references unknown query ${queryId}`);
114
+ }
115
+ if (type === "BSLFilter" && props.success) {
116
+ for (const queryId of props.data.targetQueries) {
117
+ if (!queries[queryId]) errors.push(`BSLFilter ${id} references unknown query ${queryId}`);
118
+ }
119
+ }
120
+ }
121
+ if (errors.length > 0) return { spec: null, errors };
122
+ return { spec: base.spec, errors: [] };
123
+ }
124
+ function validateDashboardSpec(value) {
125
+ const result = parseDashboardSpec(value);
126
+ return { ok: result.spec !== null, errors: result.errors };
127
+ }
128
+ export {
129
+ parseDashboardSpec,
130
+ validateDashboardSpec
131
+ };
@@ -0,0 +1,146 @@
1
+ import { GeneratedPaneSpec, GeneratedPaneElementSpec } from '@hachej/boring-generated-pane/shared';
2
+ import { z } from 'zod';
3
+
4
+ declare const dashboardQuerySchema: z.ZodUnion<readonly [z.ZodObject<{
5
+ id: z.ZodString;
6
+ model: z.ZodString;
7
+ query: z.ZodString;
8
+ limit: z.ZodOptional<z.ZodNumber>;
9
+ }, z.core.$strip>, z.ZodObject<{
10
+ id: z.ZodString;
11
+ source: z.ZodOptional<z.ZodString>;
12
+ sql: z.ZodString;
13
+ params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
14
+ limit: z.ZodOptional<z.ZodNumber>;
15
+ }, z.core.$strip>]>;
16
+ declare const dashboardGridPropsSchema: z.ZodObject<{
17
+ title: z.ZodOptional<z.ZodString>;
18
+ columns: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<1>, z.ZodLiteral<2>, z.ZodLiteral<3>, z.ZodLiteral<4>, z.ZodLiteral<6>, z.ZodLiteral<12>]>>;
19
+ }, z.core.$strip>;
20
+ declare const bslMetricPropsSchema: z.ZodObject<{
21
+ queryId: z.ZodString;
22
+ valueField: z.ZodString;
23
+ label: z.ZodString;
24
+ format: z.ZodOptional<z.ZodEnum<{
25
+ number: "number";
26
+ currency: "currency";
27
+ percent: "percent";
28
+ }>>;
29
+ }, z.core.$strip>;
30
+ declare const bslChartPropsSchema: z.ZodObject<{
31
+ queryId: z.ZodString;
32
+ title: z.ZodOptional<z.ZodString>;
33
+ renderer: z.ZodOptional<z.ZodEnum<{
34
+ echarts: "echarts";
35
+ "vega-lite": "vega-lite";
36
+ plotly: "plotly";
37
+ }>>;
38
+ chartType: z.ZodEnum<{
39
+ bar: "bar";
40
+ line: "line";
41
+ area: "area";
42
+ scatter: "scatter";
43
+ heatmap: "heatmap";
44
+ pie: "pie";
45
+ treemap: "treemap";
46
+ sunburst: "sunburst";
47
+ gauge: "gauge";
48
+ table: "table";
49
+ }>;
50
+ x: z.ZodOptional<z.ZodString>;
51
+ y: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
52
+ color: z.ZodOptional<z.ZodString>;
53
+ controls: z.ZodOptional<z.ZodArray<z.ZodString>>;
54
+ }, z.core.$strip>;
55
+ declare const bslPerspectiveViewerPropsSchema: z.ZodObject<{
56
+ queryId: z.ZodString;
57
+ title: z.ZodOptional<z.ZodString>;
58
+ plugin: z.ZodOptional<z.ZodEnum<{
59
+ Datagrid: "Datagrid";
60
+ "Y Bar": "Y Bar";
61
+ "X Bar": "X Bar";
62
+ "Y Line": "Y Line";
63
+ "Y Area": "Y Area";
64
+ "Y Scatter": "Y Scatter";
65
+ "Y Treemap": "Y Treemap";
66
+ Sunburst: "Sunburst";
67
+ Heatmap: "Heatmap";
68
+ }>>;
69
+ columns: z.ZodOptional<z.ZodArray<z.ZodString>>;
70
+ groupBy: z.ZodOptional<z.ZodArray<z.ZodString>>;
71
+ splitBy: z.ZodOptional<z.ZodArray<z.ZodString>>;
72
+ sort: z.ZodOptional<z.ZodArray<z.ZodTuple<[z.ZodString, z.ZodEnum<{
73
+ asc: "asc";
74
+ desc: "desc";
75
+ }>], null>>>;
76
+ }, z.core.$strip>;
77
+ declare const bslFilterPropsSchema: z.ZodObject<{
78
+ id: z.ZodString;
79
+ field: z.ZodString;
80
+ label: z.ZodOptional<z.ZodString>;
81
+ controlType: z.ZodEnum<{
82
+ select: "select";
83
+ multiSelect: "multiSelect";
84
+ dateRange: "dateRange";
85
+ numberRange: "numberRange";
86
+ search: "search";
87
+ }>;
88
+ targetQueries: z.ZodArray<z.ZodString>;
89
+ }, z.core.$strip>;
90
+ declare const bslTextPropsSchema: z.ZodObject<{
91
+ markdown: z.ZodString;
92
+ }, z.core.$strip>;
93
+ type DashboardGridProps = z.infer<typeof dashboardGridPropsSchema>;
94
+ type BslMetricProps = z.infer<typeof bslMetricPropsSchema>;
95
+ type BslChartProps = z.infer<typeof bslChartPropsSchema>;
96
+ type BslPerspectiveViewerProps = z.infer<typeof bslPerspectiveViewerPropsSchema>;
97
+ type BslFilterProps = z.infer<typeof bslFilterPropsSchema>;
98
+ type BslTextProps = z.infer<typeof bslTextPropsSchema>;
99
+ type BslDashboardQuerySpec = z.infer<typeof dashboardQuerySchema>;
100
+
101
+ type BslChartRenderer = BslChartProps["renderer"];
102
+ type BslChartType = BslChartProps["chartType"];
103
+ type PerspectiveViewerPlugin = BslPerspectiveViewerProps["plugin"];
104
+ interface BiDashboardSpec extends Omit<GeneratedPaneSpec, "profile" | "queries" | "elements"> {
105
+ profile: "bi-dashboard";
106
+ queries: Record<string, BslDashboardQuerySpec>;
107
+ elements: Record<string, BiDashboardElementSpec>;
108
+ }
109
+ type BslDashboardSpec = BiDashboardSpec;
110
+ type BslDashboardComponentSpec = BiDashboardElementSpec;
111
+ type BiDashboardElementSpec = DashboardGridSpec | BslMetricSpec | BslChartSpec | BslPerspectiveViewerSpec | BslFilterControlSpec | BslTextSpec;
112
+ interface BaseElementSpec extends GeneratedPaneElementSpec {
113
+ props: Record<string, unknown>;
114
+ }
115
+ interface DashboardGridSpec extends BaseElementSpec {
116
+ type: "DashboardGrid";
117
+ props: DashboardGridProps;
118
+ children: string[];
119
+ }
120
+ interface BslMetricSpec extends BaseElementSpec {
121
+ type: "BSLMetric";
122
+ props: BslMetricProps;
123
+ children?: [];
124
+ }
125
+ interface BslChartSpec extends BaseElementSpec {
126
+ type: "BSLChart";
127
+ props: BslChartProps;
128
+ children?: [];
129
+ }
130
+ interface BslPerspectiveViewerSpec extends BaseElementSpec {
131
+ type: "BSLPerspectiveViewer";
132
+ props: BslPerspectiveViewerProps;
133
+ children?: [];
134
+ }
135
+ interface BslFilterControlSpec extends BaseElementSpec {
136
+ type: "BSLFilter";
137
+ props: BslFilterProps;
138
+ children?: [];
139
+ }
140
+ interface BslTextSpec extends BaseElementSpec {
141
+ type: "BSLText";
142
+ props: BslTextProps;
143
+ children?: [];
144
+ }
145
+
146
+ export type { BslDashboardSpec as B, DashboardGridSpec as D, PerspectiveViewerPlugin as P, BslChartSpec as a, BslDashboardComponentSpec as b, BslDashboardQuerySpec as c, BslPerspectiveViewerSpec as d, BslChartRenderer as e, BslChartType as f, BslFilterControlSpec as g, BslMetricSpec as h, BslTextSpec as i };
@@ -0,0 +1,349 @@
1
+ # BI Dashboard Plugin Runtime Plan
2
+
3
+ ## Status
4
+
5
+ Follow-up plan for `@hachej/boring-bi-dashboard` after the dashboard authoring
6
+ skeleton PR. This plan assumes WorkspaceBridge RPC v1 from PR #71 and the new
7
+ `@hachej/boring-data-bridge` plugin.
8
+
9
+ ## Goal
10
+
11
+ Turn agent-authored `boring.generated-pane` JSON specs with `profile: "bi-dashboard"` into live dashboards while keeping
12
+ agents and UI components provider-neutral.
13
+
14
+ The BI dashboard plugin owns dashboard authoring/rendering. It does **not** own
15
+ provider credentials, raw SQL execution, BSL server setup, or Perspective server
16
+ transport. Those are supplied by data-bridge adapters.
17
+
18
+ ## Current state
19
+
20
+ The current plugin skeleton provides:
21
+
22
+ - controlled component catalog:
23
+ - `DashboardGrid`
24
+ - `BSLMetric`
25
+ - `BSLChart`
26
+ - `BSLPerspectiveViewer`
27
+ - `BSLFilter`
28
+ - `BSLText`
29
+ - shared generated-pane dashboard TypeScript types
30
+ - runtime validation before rendering untyped pane params
31
+ - `bi-dashboard-authoring` skill
32
+ - workspace-playground eval where the agent simply creates a dashboard
33
+
34
+ It currently renders placeholders and a query manifest. It does not execute data
35
+ queries.
36
+
37
+ ## Non-goals
38
+
39
+ - Do not add BI-dashboard-specific data endpoints.
40
+ - Do not make dashboard specs provider-specific.
41
+ - Do not ask agents to write raw React, ECharts, Perspective JS, or SQL.
42
+ - Do not duplicate data-bridge adapter logic inside the dashboard plugin.
43
+ - Do not require live Perspective server mode for the first runtime version.
44
+
45
+ ## Dashboard contract evolution
46
+
47
+ Keep the authoring contract high-level:
48
+
49
+ ```json
50
+ {
51
+ "kind": "boring.generated-pane",
52
+ "profile": "bi-dashboard",
53
+ "version": 1,
54
+ "title": "Orders Revenue",
55
+ "queries": {
56
+ "revenue_by_month": {
57
+ "id": "revenue_by_month",
58
+ "model": "orders",
59
+ "groupBy": ["month"],
60
+ "measures": ["revenue"]
61
+ }
62
+ },
63
+ "root": "dashboard",
64
+ "elements": {
65
+ "dashboard": { "type": "DashboardGrid", "props": { "columns": 2 }, "children": ["chart"] },
66
+ "chart": {
67
+ "type": "BSLChart",
68
+ "props": { "queryId": "revenue_by_month", "renderer": "echarts", "chartType": "line", "x": "month", "y": "revenue" }
69
+ }
70
+ }
71
+ }
72
+ ```
73
+
74
+ This query shape compiles to data-bridge query input:
75
+
76
+ ```ts
77
+ {
78
+ language: "bsl-dashboard",
79
+ model: "orders",
80
+ groupBy: ["month"],
81
+ measures: ["revenue"]
82
+ }
83
+ ```
84
+
85
+ The data-bridge dashboard query shape must stay isomorphic with
86
+ `BslDashboardQuerySpec`: `filters` are `{ field, op, value }[]`, and `orderBy` is
87
+ `[field, direction][]`. The BSL data-bridge adapter can then compile to BSL's
88
+ native query string:
89
+
90
+ ```python
91
+ sm.group_by("month").aggregate("revenue")
92
+ ```
93
+
94
+ Use BSL's existing query-string parser/executor rather than creating a second
95
+ BSL execution DSL. Direct `bsl-python` strings are a trusted data-bridge feature,
96
+ not what dashboard browser rendering sends by default.
97
+
98
+ ## Runtime architecture
99
+
100
+ ```txt
101
+ Dashboard JSON file / pane params
102
+ ↓ validate boring.generated-pane + bi-dashboard profile
103
+ BiDashboardPane
104
+ ↓ collect queryIds used by visible components
105
+ DataBridge browser/runtime client
106
+ ↓ WorkspaceBridge RPC data.v1.*
107
+ DataBridge trusted server plugin
108
+ ↓ per-query source resolution by host config/model id or explicit spec dataSource
109
+ ↓ selected adapter: BSL / DuckDB / Macro / static file
110
+ Portable result / Perspective descriptor
111
+
112
+ BSLMetric, BSLChart, BSLPerspectiveViewer render live UI
113
+ ```
114
+
115
+ ## Data source resolution
116
+
117
+ Runtime work must choose a deterministic source routing rule before fetching
118
+ data. Use this order:
119
+
120
+ 1. Optional future `spec.dataSource` if the dashboard author/host provides one.
121
+ 2. Host-configured model routing, e.g. `orders -> bsl-main`, `macro_series -> macro`.
122
+ 3. A single default data-bridge adapter if the host configured exactly one.
123
+ 4. Otherwise fail closed with an actionable "ambiguous data source" error.
124
+
125
+ Do not silently try every adapter for live dashboards unless the host explicitly
126
+ enables adapter probing for that workspace; probing can leak model names and make
127
+ failures non-deterministic. Source resolution is per query, not per dashboard,
128
+ because a valid dashboard may combine models routed to different adapters.
129
+
130
+ ## Data access
131
+
132
+ ### Metrics and charts
133
+
134
+ Use `data.v1.query.run`:
135
+
136
+ ```ts
137
+ const query = spec.queries[queryId]
138
+ bridge.call("data.v1.query.run", {
139
+ source: resolveDashboardDataSource(spec, query),
140
+ query: compileDashboardQuery(query),
141
+ })
142
+ ```
143
+
144
+ Return:
145
+
146
+ ```ts
147
+ DataBridgeTableResult
148
+ ```
149
+
150
+ Rendering rules:
151
+
152
+ - `BSLMetric`: first row + `valueField`.
153
+ - `BSLChart`: rows + declarative chart props → ECharts option generated inside
154
+ plugin.
155
+ - `BSLFilter`: updates local dashboard filter state and re-runs affected query
156
+ ids.
157
+
158
+ ### Perspective panels
159
+
160
+ For plain `workspace-file` dashboard queries, use the file data/Perspective endpoint once available. For semantic, BSL, DuckDB, SQL, or remote-adapter queries, use `data.v1.perspective.prepare`:
161
+
162
+ ```ts
163
+ const query = spec.queries[component.props.queryId]
164
+ if (query.dataRef?.kind === "workspace-file") {
165
+ fileData.preparePerspective({ path: query.dataRef.path, viewer, transport })
166
+ } else {
167
+ bridge.call("data.v1.perspective.prepare", {
168
+ source: resolveDashboardDataSource(spec, query),
169
+ datasetId: component.props.queryId,
170
+ query: compileDashboardQuery(query),
171
+ viewer: {
172
+ plugin: component.props.plugin,
173
+ columns: component.props.columns,
174
+ group_by: component.props.groupBy,
175
+ split_by: component.props.splitBy,
176
+ sort: component.props.sort,
177
+ filter: compilePerspectiveFilters(component.props.filters, activeDashboardFilters),
178
+ },
179
+ transport: {
180
+ preferred: "auto",
181
+ accepted: ["inline", "artifact", "websocket"],
182
+ payloadFormat: "arrow",
183
+ maxInlineBytes: 1_000_000,
184
+ },
185
+ })
186
+ }
187
+ ```
188
+
189
+ The dashboard advertises supported transports and preferences; the server
190
+ selects the actual safe transport. First runtime version may load inline
191
+ JSON/Arrow into `@finos/perspective` in browser/WASM; larger static results
192
+ should use Arrow artifacts, and live/server mode should use websocket.
193
+
194
+ Perspective filter mapping is explicit: component `props.filters` and active
195
+ `BSLFilter` state are merged as `BslFilterExpression[]`, then converted to
196
+ Perspective viewer filters. Scalar comparisons map directly; `in` expands to the
197
+ adapter-supported equivalent; `between` maps to two filters (`>=` and `<=`); and
198
+ unsupported `contains` behavior must fail visibly instead of being dropped.
199
+
200
+ Later proper client/server replicated mode follows Perspective's architecture:
201
+
202
+ ```txt
203
+ server Perspective table
204
+ ↔ websocket + Arrow deltas
205
+ browser Perspective websocket client
206
+ → server_table.view()
207
+ → local WASM worker.table(server_view)
208
+ → <perspective-viewer>.load(client_table)
209
+ ```
210
+
211
+ The dashboard plugin should consume a returned descriptor; it should not decide
212
+ how the server table is created.
213
+
214
+ ## Frontend components
215
+
216
+ ### `useDashboardData(spec, filters)`
217
+
218
+ Responsibilities:
219
+
220
+ - validate spec already passed
221
+ - discover query dependencies by component type
222
+ - dedupe concurrent requests by query id and filter key
223
+ - call data-bridge
224
+ - expose `{ dataByQueryId, loadingByQueryId, errorByQueryId, refresh }`
225
+
226
+ ### `BSLMetricRenderer`
227
+
228
+ - receives `DataBridgeTableResult | undefined`
229
+ - formats number/currency/percent
230
+ - shows loading/error/empty states
231
+
232
+ ### `BSLChartRenderer`
233
+
234
+ - receives `DataBridgeTableResult | undefined`
235
+ - maps declarative chart props to ECharts options
236
+ - keeps chart option generation local and deterministic
237
+ - never accepts raw ECharts options from dashboard JSON
238
+
239
+ ### `BSLPerspectiveRuntimeViewer`
240
+
241
+ - calls `data.v1.perspective.prepare` or consumes prepared descriptor
242
+ - supports inline dataset first
243
+ - later supports websocket descriptor
244
+ - maps camelCase dashboard props to Perspective's `group_by`/`split_by` config
245
+
246
+ ## Server integration
247
+
248
+ The dashboard plugin should not register its own data handlers. It should depend
249
+ on data-bridge being installed by the host. If data-bridge is missing, render a
250
+ clear empty state:
251
+
252
+ ```txt
253
+ Live data is unavailable because @hachej/boring-data-bridge is not installed.
254
+ The dashboard spec is valid and can still be edited.
255
+ ```
256
+
257
+ Workspace-playground should install:
258
+
259
+ ```txt
260
+ @hachej/boring-bi-dashboard
261
+ @hachej/boring-data-bridge
262
+ ```
263
+
264
+ and configure data-bridge with a DuckDB/static adapter over fixture CSV files.
265
+
266
+ BSL-backed apps should configure data-bridge with a BSL adapter using model path
267
+ and profile settings.
268
+
269
+ ## Agent skill update
270
+
271
+ The `bi-dashboard-authoring` skill should stay brief. It should teach:
272
+
273
+ - create `dashboards/*.dashboard.json`
274
+ - use `kind: boring.generated-pane and profile: bi-dashboard`, `version: 1`
275
+ - use the controlled components and exact prop names
276
+ - author semantic dashboard queries with `model`, `groupBy`, `measures`, etc.
277
+ - do not write raw React/ECharts/Perspective
278
+
279
+ It should not include provider setup details. Provider details belong in a data
280
+ source/data-bridge skill if needed.
281
+
282
+ ## Implementation phases
283
+
284
+ ### Phase 0 — Contract validator hardening
285
+
286
+ - Extend `validateDashboardSpec` before any runtime data execution.
287
+ - Validate query `filters`, query `orderBy`, and query `limit` shapes.
288
+ - Validate `BSLPerspectiveViewer.props.filters` with the same `BslFilterExpression` rules.
289
+ - Add regression tests proving malformed filters/order/limits are rejected before data-bridge compilation.
290
+
291
+ ### Phase 1 — Data-bridge client seam
292
+
293
+ - Add a tiny dashboard data client abstraction.
294
+ - In tests, mock it rather than mocking `fetch` directly.
295
+ - Add missing-data empty states.
296
+
297
+ ### Phase 2 — Query execution for metrics/charts
298
+
299
+ - Compile dashboard query shape to data-bridge `bsl-dashboard` input.
300
+ - Fetch `data.v1.query.run` for `BSLMetric` and `BSLChart`.
301
+ - Render real metric values.
302
+ - Add minimal ECharts runtime for line/bar/heatmap/table, or a placeholder until
303
+ ECharts dependency is explicitly accepted.
304
+
305
+ ### Phase 3 — Filters
306
+
307
+ - Implement `BSLFilter` state.
308
+ - Re-run only target queries.
309
+ - Encode filter state in the data-bridge query input.
310
+
311
+ ### Phase 4 — Perspective negotiated runtime
312
+
313
+ - Add Perspective dependency behind the BI dashboard plugin.
314
+ - Call `data.v1.perspective.prepare` with `transport.preferred: "auto"`, accepted transports, payload format, and max inline bytes.
315
+ - Load returned inline JSON/Arrow into browser Perspective worker/viewer for small results.
316
+ - Load returned Arrow artifacts for larger static results once artifact support exists.
317
+
318
+ ### Phase 5 — Perspective replicated mode
319
+
320
+ - Accept data-bridge websocket descriptors.
321
+ - Use Perspective websocket client + browser WASM replication.
322
+ - Keep client/server table lifecycle in data-bridge/Perspective adapter.
323
+
324
+ ### Phase 6 — Evals and validation
325
+
326
+ - Keep the main eval simple: "Create a BI dashboard...".
327
+ - Add separate advanced evals for stress cases, but make them validate generated
328
+ JSON with the shared parser.
329
+ - Add runtime tests for invalid specs, data errors, filter updates, and
330
+ Perspective descriptor loading.
331
+
332
+ ## Validation
333
+
334
+ - `@hachej/boring-bi-dashboard` typecheck/test/build.
335
+ - Contract tests for dashboard query compilation.
336
+ - Validator tests for query filters/orderBy/limit and Perspective viewer filters.
337
+ - Component tests with mocked data-bridge client.
338
+ - WorkspaceBridge e2e with data-bridge installed.
339
+ - Generated dashboard eval followed by `validateDashboardSpec`.
340
+ - Advanced dashboard evals for SaaS, supply chain, finance, and operations.
341
+
342
+ ## Open questions
343
+
344
+ - Should `dataSource` be added to `BslDashboardSpec` in v1.1, or kept entirely
345
+ host-configured for portability? Runtime v1 should support both explicit and
346
+ host-configured source resolution as described above.
347
+ - Which ECharts subset is accepted for v1: line/bar/heatmap/table only, or more?
348
+ - Should inline Perspective be allowed for large results, or should data-bridge
349
+ force artifact/websocket above a row threshold?