@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,425 @@
1
+ # Data Bridge Plugin Plan
2
+
3
+ ## Status
4
+
5
+ Draft plan for a reusable `@hachej/boring-data-bridge` plugin built on top of
6
+ WorkspaceBridge RPC v1 from PR #71. This plugin should replace ad-hoc product
7
+ bridges such as Macro's custom workspace bridge over time, while keeping the
8
+ workspace package domain-neutral.
9
+
10
+ ## Goal
11
+
12
+ Create a trusted, reusable data-access plugin that exposes stable semantic data
13
+ operations through WorkspaceBridge, implemented by provider adapters.
14
+
15
+ The bridge answers one question:
16
+
17
+ > Given a semantic data request, which installed adapter can satisfy it safely,
18
+ > and what portable result/artifact should the caller receive?
19
+
20
+ ## Non-goals
21
+
22
+ - Do not add a new generic `/api/data-request/*` HTTP endpoint.
23
+ - Do not make `@hachej/boring-workspace` understand SQL, BSL, Macro, or
24
+ Perspective. Plain tabular file parsing should live in a separate server-only
25
+ `@hachej/file-data` package or a deliberately accepted workspace file-data
26
+ module, not inside data-bridge itself.
27
+ - Do not expose provider-bound operations such as `clickhouse.v1.query`,
28
+ `duckdb.v1.query`, or `bsl.v1.execute` as the BI/dashboard API.
29
+ - Do not let generated/user runtime plugins self-register host bridge handlers.
30
+ - Do not make agents generate raw Perspective, ECharts, DuckDB, or ClickHouse
31
+ configs.
32
+
33
+ ## Dependencies
34
+
35
+ - PR #71 WorkspaceBridge RPC v1:
36
+ - `/api/v1/workspace-bridge/call`
37
+ - registered `workspaceBridgeHandlers`
38
+ - `WorkspaceBridgeClient.fromEnv()` for runtime callers
39
+ - runtime env: `BORING_WORKSPACE_BRIDGE_URL`, `BORING_WORKSPACE_BRIDGE_TOKEN`,
40
+ `BORING_WORKSPACE_ID`, `BORING_AGENT_SESSION_ID`
41
+ - caller classes, capabilities, schema limits, timeouts, idempotency
42
+ - BSL's existing LLM query mechanism:
43
+ - `BSLTools.query_model` accepts an Ibis-style BSL query string such as
44
+ `sm.group_by("origin").aggregate("flight_count")`
45
+ - this should be used by the BSL adapter rather than inventing a second JSON
46
+ DSL for BSL execution
47
+ - BSL Perspective artifact contract, used by the BSL adapter as an internal/nested payload rather than the top-level WorkspaceBridge response:
48
+ - `kind: bsl.perspective.dataset` for inline data
49
+ - `kind: bsl.perspective.artifact` with `data_ref` for external JSON/Arrow
50
+
51
+ ## Package shape
52
+
53
+ ```txt
54
+ plugins/data-bridge/
55
+ package.json
56
+ src/shared/
57
+ contracts.ts
58
+ adapters.ts
59
+ index.ts
60
+ src/server/
61
+ index.ts
62
+ handlers.ts
63
+ registry.ts
64
+ adapters/
65
+ staticFileAdapter.ts
66
+ bslAdapter.ts
67
+ duckdbAdapter.ts
68
+ perspectiveAdapter.ts
69
+ skills/data-bridge-usage/SKILL.md
70
+ ```
71
+
72
+ Package name:
73
+
74
+ ```json
75
+ "name": "@hachej/boring-data-bridge"
76
+ ```
77
+
78
+ The package is primarily a **trusted server plugin**. It may also export shared
79
+ client helpers, but the transport is WorkspaceBridge.
80
+
81
+ ## Core contract
82
+
83
+ ### Data bridge request identity
84
+
85
+ Every data request should be provider-neutral and adapter-routable:
86
+
87
+ ```ts
88
+ interface DataBridgeRequestBase {
89
+ requestId?: string
90
+ source?: string // optional adapter id or logical source id
91
+ workspaceId?: string // normally inferred by WorkspaceBridge context
92
+ }
93
+ ```
94
+
95
+ The caller may set `source` when the dashboard/model knows a logical source
96
+ (`bsl`, `playground`, `macro`). If omitted, the bridge asks adapters whether they
97
+ can satisfy the request.
98
+
99
+ ### Portable tabular result
100
+
101
+ ```ts
102
+ interface DataBridgeColumn {
103
+ name: string
104
+ type: "string" | "integer" | "float" | "boolean" | "date" | "datetime" | "json"
105
+ role?: "dimension" | "measure" | "time" | "unknown"
106
+ }
107
+
108
+ interface DataBridgeTableResult {
109
+ kind: "data-bridge.table"
110
+ version: 1
111
+ columns: DataBridgeColumn[]
112
+ rows: Record<string, unknown>[]
113
+ rowCount: number
114
+ truncated?: boolean
115
+ source?: string
116
+ }
117
+ ```
118
+
119
+ ### Semantic BSL query request
120
+
121
+ Prefer BSL's existing Python/Ibis-style query string mechanism:
122
+
123
+ ```ts
124
+ interface DataBridgeSemanticQuery {
125
+ language: "bsl-python"
126
+ model: string
127
+ query: string // e.g. 'sm.group_by("month").aggregate("revenue")'
128
+ parameters?: Record<string, unknown>
129
+ limit?: number
130
+ }
131
+ ```
132
+
133
+ Direct `language: "bsl-python"` is **not** the default browser/runtime dashboard
134
+ path. It is a code-like semantic query language and must be separately gated:
135
+
136
+ - allowed caller classes: `server` and trusted `runtime` only by default
137
+ - required capability: `data:bsl-query-string` in addition to `data:read`
138
+ - never accepted from generic browser dashboard rendering calls
139
+ - parsed/validated by the BSL adapter before execution; unsupported names,
140
+ imports, attributes outside the BSL query surface, multi-statements, and file/OS
141
+ access are rejected
142
+
143
+ Dashboard/browser usage should send `language: "bsl-dashboard"`; the trusted BSL
144
+ adapter compiles that safe structured query into the native BSL query-string path.
145
+
146
+ ### Dashboard query request
147
+
148
+ For agent-authored dashboards, keep the high-level shape and compile in the
149
+ adapter/plugin layer:
150
+
151
+ ```ts
152
+ interface DataBridgeFilterExpression {
153
+ field: string
154
+ op: "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "in" | "contains" | "between"
155
+ value: unknown
156
+ }
157
+
158
+ type DataRef =
159
+ | { kind: "workspace-file"; path: string; fileFormat?: "csv" | "json" | "ndjson" | "parquet" | "arrow"; limit?: number }
160
+ | { kind: "duckdb-file"; path: string; table?: string }
161
+ | { kind: "sqlite-file"; path: string; table?: string }
162
+ | { kind: "semantic-model"; model: string }
163
+
164
+ interface DataBridgeDashboardQuery {
165
+ language: "bsl-dashboard"
166
+ model: string
167
+ dataRef?: DataRef
168
+ groupBy?: string[]
169
+ measures?: string[]
170
+ dimensions?: string[]
171
+ filters?: DataBridgeFilterExpression[]
172
+ orderBy?: Array<[field: string, direction: "asc" | "desc"]>
173
+ limit?: number
174
+ }
175
+ ```
176
+
177
+ The BSL adapter compiles this to the BSL query string form, for example:
178
+
179
+ ```txt
180
+ sm.group_by("month", "region").aggregate("revenue", "order_count")
181
+ ```
182
+
183
+ This keeps dashboard specs easy for agents while still using BSL's real query
184
+ mechanism under the hood. The `filters` and `orderBy` shapes intentionally match
185
+ `BslDashboardQuerySpec` so the BI dashboard compiler is lossless. Filter mapping
186
+ is explicit: `eq/neq/gt/gte/lt/lte` compile to scalar comparisons, `in` compiles
187
+ to membership, `contains` compiles to the adapter's supported string contains
188
+ operation, and `between` requires a two-value tuple/array and compiles to
189
+ `gte && lte`.
190
+
191
+ ## WorkspaceBridge operations
192
+
193
+ Use stable `data.v1.*` operation names. These are not provider actions; they are
194
+ semantic data bridge capabilities implemented by adapters.
195
+
196
+ ### `data.v1.catalog.search`
197
+
198
+ Search available datasets/models/series.
199
+
200
+ ```ts
201
+ input: {
202
+ source?: string
203
+ query: string
204
+ limit?: number
205
+ filters?: Record<string, string | string[]>
206
+ }
207
+ output: {
208
+ items: Array<{ id: string; title: string; subtitle?: string; source?: string; tags?: string[] }>
209
+ total?: number
210
+ hasMore?: boolean
211
+ }
212
+ ```
213
+
214
+ ### `data.v1.dataset.preview`
215
+
216
+ Preview a dataset by id/path/model without requiring a semantic query.
217
+
218
+ ```ts
219
+ input: { source?: string; datasetId: string; limit?: number; offset?: number }
220
+ output: DataBridgeTableResult
221
+ ```
222
+
223
+ ### `data.v1.query.run`
224
+
225
+ Run a semantic query and return tabular data.
226
+
227
+ ```ts
228
+ interface DataBridgeSqlQuery {
229
+ language: "sql"
230
+ dialect: "duckdb" | "sqlite" | string
231
+ sql: string
232
+ dataRef?: Extract<DataRef, { kind: "duckdb-file" | "sqlite-file" }>
233
+ limit?: number
234
+ }
235
+
236
+ input: {
237
+ source?: string
238
+ query: DataBridgeDashboardQuery | DataBridgeSemanticQuery | DataBridgeSqlQuery
239
+ }
240
+
241
+ `DataBridgeSemanticQuery` and `DataBridgeSqlQuery` require handler-level caller/capability checks in addition to the operation definition. Because WorkspaceBridge capabilities are declared per operation, `data.v1.query.run` must still inspect `input.query.language` and reject `bsl-python` unless `context.callerClass` is trusted and `context.capabilities` includes `data:bsl-query-string`. Direct SQL requires a separate `data:sql-query` capability unless the adapter compiles from the safe `bsl-dashboard` shape. Browser dashboard callers are restricted to `DataBridgeDashboardQuery`.
242
+ output: DataBridgeTableResult
243
+ ```
244
+
245
+ ### `data.v1.perspective.prepare`
246
+
247
+ Prepare a Perspective-compatible dataset or server table descriptor from a
248
+ semantic query. The consumer advertises acceptable transports; the server chooses
249
+ the safe concrete transport. Keep this descriptor shape aligned with
250
+ `plugins/bi-dashboard/docs/issues/bi-dashboard-plugin/data-access-unification.md`.
251
+
252
+ ```ts
253
+ type DataTransport = "inline" | "artifact" | "websocket"
254
+ type PreferredDataTransport = "auto" | DataTransport
255
+ type PayloadFormat = "arrow" | "json"
256
+
257
+ interface PerspectiveViewerConfig {
258
+ plugin?: string
259
+ columns?: string[]
260
+ group_by?: string[]
261
+ split_by?: string[]
262
+ sort?: Array<[string, "asc" | "desc"]>
263
+ filter?: unknown[][]
264
+ aggregates?: Record<string, string>
265
+ }
266
+
267
+ input: {
268
+ source?: string
269
+ datasetId?: string
270
+ query: DataBridgeSemanticQuery | DataBridgeDashboardQuery | DataBridgeSqlQuery
271
+ viewer?: PerspectiveViewerConfig
272
+ transport?: {
273
+ preferred?: PreferredDataTransport
274
+ accepted: DataTransport[]
275
+ payloadFormat?: PayloadFormat
276
+ maxInlineBytes?: number
277
+ }
278
+ }
279
+ output: {
280
+ kind: "data-bridge.perspective"
281
+ version: 1
282
+ transport: DataTransport
283
+ payloadFormat: PayloadFormat
284
+ schema?: DataBridgeColumn[]
285
+ rowCount?: number
286
+ source?: string
287
+ viewer?: PerspectiveViewerConfig
288
+ inline?: { bytes?: number; data?: unknown; base64?: string }
289
+ artifact?: { url: string; contentType: string; expiresAt?: string; bytes?: number }
290
+ websocket?: { url: string; protocol: "perspective" | "data-bridge-arrow-delta"; sessionId: string }
291
+ }
292
+ ```
293
+
294
+ Start with inline JSON/Arrow for small results, add artifact Arrow for larger
295
+ static results, and add websocket when a Perspective server runtime is
296
+ available.
297
+
298
+ ## Adapter API
299
+
300
+ ```ts
301
+ interface DataBridgeAdapter {
302
+ id: string
303
+ label: string
304
+ capabilities: {
305
+ catalog?: boolean
306
+ datasetPreview?: boolean
307
+ semanticQuery?: boolean
308
+ perspective?: boolean
309
+ }
310
+
311
+ canHandle(input: unknown, context: DataBridgeContext): boolean | Promise<boolean>
312
+ searchCatalog?(input: CatalogSearchInput, context: DataBridgeContext): Promise<CatalogSearchOutput>
313
+ previewDataset?(input: DatasetPreviewInput, context: DataBridgeContext): Promise<DataBridgeTableResult>
314
+ runQuery?(input: QueryRunInput, context: DataBridgeContext): Promise<DataBridgeTableResult>
315
+ preparePerspective?(input: PerspectivePrepareInput, context: DataBridgeContext): Promise<PerspectivePrepareOutput>
316
+ }
317
+ ```
318
+
319
+ Adapters are explicitly installed by host/trusted plugins. Provider-specific
320
+ credentials stay inside adapters.
321
+
322
+ ## Initial adapters
323
+
324
+ 1. **Static/workspace file adapter**
325
+ - CSV/JSON/NDJSON preview and small dashboard aggregations delegate to the
326
+ shared `@hachej/file-data` implementation used by `/api/v1/files/records`.
327
+ - Never parse CSV/JSON independently and never read raw `workspaceRoot` paths
328
+ directly; use workspace adapter/file-data semantics.
329
+ - Useful for playground/evals and dashboards over local artifacts.
330
+
331
+ 2. **DuckDB adapter**
332
+ - Read-only SQL over registered workspace datasets.
333
+ - This adapter may support `language: "sql"` internally, but BI dashboard
334
+ should prefer semantic/dashboard query inputs.
335
+
336
+ 3. **BSL adapter**
337
+ - Executes BSL via existing `BSLTools`/query-string machinery.
338
+ - Accepts `language: "bsl-dashboard"` from normal dashboard/browser calls.
339
+ - Accepts direct `language: "bsl-python"` only after the handler checks
340
+ caller class and `data:bsl-query-string` capability.
341
+ - Compiles `language: "bsl-dashboard"` into BSL query strings.
342
+ - Can produce Perspective inline/artifact outputs using BSL's Perspective
343
+ helpers.
344
+
345
+ 4. **Macro adapter**
346
+ - Wraps Macro's catalog/search/series services behind `data.v1.*` ops.
347
+ - Migration target for Macro's custom `/api/macro/workspace-bridge/call`.
348
+
349
+ 5. **Perspective adapter/decorator**
350
+ - Converts `DataBridgeTableResult` or BSL result into
351
+ `bsl.perspective.dataset`/artifact.
352
+ - Later owns server-side Perspective table registry and websocket descriptor.
353
+
354
+ ## Security and safety
355
+
356
+ - Register handlers via `workspaceBridgeHandlers` only from trusted server
357
+ plugins.
358
+ - Operation definitions set small defaults and explicit max output sizes.
359
+ - Runtime calls require `data:read` or narrower capabilities.
360
+ - Mutating or artifact-writing operations require idempotency keys.
361
+ - No raw provider credentials in bridge inputs/outputs/logs.
362
+ - No arbitrary filesystem paths from runtime callers; use workspace-relative ids
363
+ and existing file validation.
364
+ - SQL support, where present, is adapter-local and read-only; not part of the
365
+ BI dashboard contract.
366
+
367
+ ## Implementation phases
368
+
369
+ ### Phase 1 — Contracts and in-memory registry
370
+
371
+ - Add package scaffold.
372
+ - Define shared contracts and adapter API.
373
+ - Add server registry with deterministic adapter selection.
374
+ - Unit-test schema validation, adapter precedence, and error envelopes.
375
+
376
+ ### Phase 2 — WorkspaceBridge handlers
377
+
378
+ - Register `data.v1.catalog.search`, `data.v1.dataset.preview`, and
379
+ `data.v1.query.run` through `workspaceBridgeHandlers`.
380
+ - Use PR #71 `defineTrustedDomainBridgeHandler` if available.
381
+ - Add an e2e smoke similar to PR #71 `bridge-e2e.ts`.
382
+
383
+ ### Phase 3 — BSL adapter
384
+
385
+ - Add compiler from dashboard query shape to BSL query string.
386
+ - Add a BSL adapter that executes BSL's native tool/query mechanism.
387
+ - Gate direct `language: "bsl-python"` calls behind `data:bsl-query-string` and
388
+ trusted caller classes.
389
+ - Validate both direct and generated query strings against an allowlist-style
390
+ parser/builder; never concatenate unchecked user code.
391
+
392
+ ### Phase 4 — Perspective prepare
393
+
394
+ - Add `data.v1.perspective.prepare`.
395
+ - Public output is the `data-bridge.perspective` descriptor. BSL
396
+ `bsl.perspective.dataset` / `bsl.perspective.artifact` objects may appear as
397
+ adapter-internal payloads inside `inline.data` or artifact metadata, but are
398
+ not the top-level WorkspaceBridge output.
399
+ - Return inline JSON/Arrow first for small results.
400
+ - Add artifact mode when file outputs are needed.
401
+ - Defer Perspective websocket server replication until the dataset path is
402
+ stable.
403
+
404
+ ### Phase 5 — Macro/playground migrations
405
+
406
+ - Adapt playground CSV data and Macro services to data-bridge adapters.
407
+ - Keep old Macro routes temporarily as compatibility shims.
408
+ - Move dashboard/data-explorer consumers to `data.v1.*` calls.
409
+
410
+ ## Validation
411
+
412
+ - Unit tests for contracts, adapter registry, and handlers.
413
+ - WorkspaceBridge e2e with browser and runtime caller classes.
414
+ - BSL adapter tests using a tiny semantic model and query string:
415
+ `sm.group_by("month").aggregate("revenue")`.
416
+ - Perspective prepare tests validating the public `data-bridge.perspective` descriptor and, for the BSL adapter, the nested BSL Perspective payload shape.
417
+ - Security tests: untrusted plugin cannot register handlers; runtime caller
418
+ without capability is rejected; oversize output is rejected.
419
+
420
+ ## Open questions
421
+
422
+ - Should direct SQL be exposed to browser callers with `data:sql-query`, or restricted to runtime/server callers only?
423
+ - Should Perspective websocket mode live in data-bridge or a separate
424
+ `perspective-bridge` adapter package?
425
+ - How should BSL model/profile discovery be configured in child apps?
@@ -0,0 +1,16 @@
1
+ *
2
+ !.gitignore
3
+ !README.md
4
+ !dashboards/
5
+ !dashboards/**
6
+ !data/
7
+ !data/**
8
+ !eval/
9
+ !eval/**
10
+ !.pi/
11
+ !.pi/extensions/
12
+ !.pi/extensions/bi-dashboard/
13
+ !.pi/extensions/bi-dashboard/package.json
14
+ !.pi/extensions/bi-dashboard/front/
15
+ !.pi/extensions/bi-dashboard/front/index.ts
16
+ .pi/extensions/bi-dashboard/.boring-signature.json
@@ -0,0 +1 @@
1
+ export { default } from "@hachej/boring-bi-dashboard/front"
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "bi-dashboard-demo-extension",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "boring": {
7
+ "id": "bi-dashboard",
8
+ "label": "BI Dashboard Demo",
9
+ "front": "front/index.ts"
10
+ }
11
+ }
@@ -0,0 +1,10 @@
1
+ # BI dashboard demo workspace
2
+
3
+ This directory is a tiny workspace fixture for the BI dashboard plugin.
4
+
5
+ - `data/people.csv` — sample tabular data.
6
+ - `dashboards/people.dashboard.json` — dashboard spec that reads the CSV through `data.v1.query.run`.
7
+ - `eval/bi-dashboard.yaml` — authoring eval prompt for dashboard generation.
8
+ - `.pi/extensions/bi-dashboard` — tiny workspace-local front extension that re-exports `@hachej/boring-bi-dashboard/front` for manual browser testing with external plugins enabled.
9
+
10
+ For live data queries in browser testing, the host must also load the trusted `@hachej/boring-data-bridge` server plugin; the plugin-local eval runner does this automatically.
@@ -0,0 +1,178 @@
1
+ {
2
+ "kind": "boring.generated-pane",
3
+ "profile": "bi-dashboard",
4
+ "version": 1,
5
+ "title": "People Operations Command Center",
6
+ "description": "An agent-authored BI dashboard rendered from safe JSON: controllers, metrics, charts, and Perspective-style tables.",
7
+ "queries": {
8
+ "total_revenue": {
9
+ "id": "total_revenue",
10
+ "source": "people-duckdb",
11
+ "sql": "SELECT sum(revenue) AS revenue FROM people"
12
+ },
13
+ "headcount": {
14
+ "id": "headcount",
15
+ "source": "people-duckdb",
16
+ "sql": "SELECT count(*) AS count FROM people"
17
+ },
18
+ "revenue_by_role": {
19
+ "id": "revenue_by_role",
20
+ "source": "people-duckdb",
21
+ "sql": "SELECT role, sum(revenue) AS revenue FROM people GROUP BY role ORDER BY revenue DESC"
22
+ },
23
+ "headcount_by_region": {
24
+ "id": "headcount_by_region",
25
+ "source": "people-duckdb",
26
+ "sql": "SELECT region, count(*) AS count FROM people GROUP BY region ORDER BY count DESC"
27
+ },
28
+ "revenue_by_region": {
29
+ "id": "revenue_by_region",
30
+ "source": "people-duckdb",
31
+ "sql": "SELECT region, sum(revenue) AS revenue FROM people GROUP BY region ORDER BY revenue DESC"
32
+ },
33
+ "people_detail": {
34
+ "id": "people_detail",
35
+ "source": "people-duckdb",
36
+ "sql": "SELECT role, region, sum(revenue) AS revenue FROM people GROUP BY role, region ORDER BY revenue DESC"
37
+ }
38
+ },
39
+ "root": "dashboard",
40
+ "elements": {
41
+ "dashboard": {
42
+ "type": "DashboardGrid",
43
+ "props": {
44
+ "columns": 12
45
+ },
46
+ "children": [
47
+ "summary",
48
+ "total-revenue",
49
+ "headcount",
50
+ "role-controller",
51
+ "region-controller",
52
+ "role-chart",
53
+ "region-chart",
54
+ "perspective-role",
55
+ "perspective-detail"
56
+ ]
57
+ },
58
+ "summary": {
59
+ "type": "BSLText",
60
+ "props": {
61
+ "markdown": "Control-plane view for a people/revenue dataset. The JSON spec defines only catalog components and data queries; the plugin decides how to render charts and Perspective-style tables."
62
+ }
63
+ },
64
+ "total-revenue": {
65
+ "type": "BSLMetric",
66
+ "props": {
67
+ "queryId": "total_revenue",
68
+ "valueField": "revenue",
69
+ "label": "Total revenue",
70
+ "format": "currency"
71
+ }
72
+ },
73
+ "headcount": {
74
+ "type": "BSLMetric",
75
+ "props": {
76
+ "queryId": "headcount",
77
+ "valueField": "count",
78
+ "label": "People",
79
+ "format": "number"
80
+ }
81
+ },
82
+ "role-controller": {
83
+ "type": "BSLFilter",
84
+ "props": {
85
+ "id": "role-filter",
86
+ "field": "role",
87
+ "label": "Role controller",
88
+ "controlType": "multiSelect",
89
+ "targetQueries": [
90
+ "revenue_by_role",
91
+ "people_detail"
92
+ ]
93
+ }
94
+ },
95
+ "region-controller": {
96
+ "type": "BSLFilter",
97
+ "props": {
98
+ "id": "region-filter",
99
+ "field": "region",
100
+ "label": "Region controller",
101
+ "controlType": "select",
102
+ "targetQueries": [
103
+ "headcount_by_region",
104
+ "revenue_by_region",
105
+ "people_detail"
106
+ ]
107
+ }
108
+ },
109
+ "role-chart": {
110
+ "type": "BSLChart",
111
+ "props": {
112
+ "queryId": "revenue_by_role",
113
+ "title": "Revenue by role",
114
+ "renderer": "echarts",
115
+ "chartType": "bar",
116
+ "x": "role",
117
+ "y": "revenue",
118
+ "controls": [
119
+ "role-filter"
120
+ ]
121
+ }
122
+ },
123
+ "region-chart": {
124
+ "type": "BSLChart",
125
+ "props": {
126
+ "queryId": "revenue_by_region",
127
+ "title": "Revenue by region",
128
+ "renderer": "echarts",
129
+ "chartType": "bar",
130
+ "x": "region",
131
+ "y": "revenue",
132
+ "controls": [
133
+ "region-filter"
134
+ ]
135
+ }
136
+ },
137
+ "perspective-role": {
138
+ "type": "BSLPerspectiveViewer",
139
+ "props": {
140
+ "queryId": "headcount_by_region",
141
+ "title": "Perspective: headcount by region",
142
+ "plugin": "Y Bar",
143
+ "columns": [
144
+ "region",
145
+ "count"
146
+ ],
147
+ "sort": [
148
+ [
149
+ "count",
150
+ "desc"
151
+ ]
152
+ ]
153
+ }
154
+ },
155
+ "perspective-detail": {
156
+ "type": "BSLPerspectiveViewer",
157
+ "props": {
158
+ "queryId": "people_detail",
159
+ "title": "Perspective: role \u00d7 region revenue",
160
+ "plugin": "Datagrid",
161
+ "columns": [
162
+ "role",
163
+ "region",
164
+ "revenue"
165
+ ],
166
+ "groupBy": [
167
+ "role"
168
+ ],
169
+ "sort": [
170
+ [
171
+ "revenue",
172
+ "desc"
173
+ ]
174
+ ]
175
+ }
176
+ }
177
+ }
178
+ }
@@ -0,0 +1,13 @@
1
+ id,role,region,revenue,utilization,satisfaction
2
+ 1,engineer,EMEA,120000,0.82,0.91
3
+ 2,designer,AMER,80000,0.74,0.88
4
+ 3,engineer,EMEA,140000,0.89,0.93
5
+ 4,analyst,APAC,90000,0.69,0.84
6
+ 5,engineer,AMER,155000,0.92,0.9
7
+ 6,designer,EMEA,98000,0.77,0.86
8
+ 7,analyst,AMER,112000,0.81,0.89
9
+ 8,product,APAC,132000,0.73,0.87
10
+ 9,product,EMEA,148000,0.86,0.92
11
+ 10,engineer,APAC,126000,0.79,0.85
12
+ 12,ops,AMER,76000,0.71,0.82
13
+ 13,ops,EMEA,88000,0.76,0.86
@@ -0,0 +1,31 @@
1
+ # Eval suite: BSL BI dashboard authoring.
2
+ #
3
+ # Goal: verify the BI dashboard plugin skill gives the agent enough context to
4
+ # create dashboard artifacts from a plain BI dashboard request.
5
+ #
6
+ # Run from the repo root:
7
+ # pnpm --filter @hachej/boring-bi-dashboard playground:eval
8
+
9
+ model:
10
+ provider: openai-codex
11
+ id: gpt-5.5
12
+
13
+ defaults:
14
+ retries: 1
15
+ timeoutMs: 180000
16
+
17
+ prompts:
18
+ - prompt: Can you make me a dashboard for orders revenue that I can open in the workspace?
19
+ expect:
20
+ - tool: write
21
+ params:
22
+ path: !EvalRegex 'dashboards/.+\.dashboard\.json'
23
+ content: !EvalRegex '"kind"\s*:\s*"boring\.generated-pane"'
24
+ - tool: write
25
+ params:
26
+ path: !EvalRegex 'dashboards/.+\.dashboard\.json'
27
+ content: !EvalRegex '"profile"\s*:\s*"bi-dashboard"'
28
+ - tool: write
29
+ params:
30
+ path: !EvalRegex 'dashboards/.+\.dashboard\.json'
31
+ content: !EvalRegex '"elements"\s*:'