@bonnard/cli 0.1.13 → 0.2.1

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.
Files changed (55) hide show
  1. package/dist/bin/bon.mjs +305 -620
  2. package/dist/bin/validate-DEh1XQnH.mjs +365 -0
  3. package/dist/docs/_index.md +1 -1
  4. package/dist/docs/topics/cubes.data-source.md +2 -2
  5. package/dist/docs/topics/cubes.dimensions.format.md +2 -2
  6. package/dist/docs/topics/cubes.dimensions.md +2 -2
  7. package/dist/docs/topics/cubes.dimensions.primary-key.md +2 -2
  8. package/dist/docs/topics/cubes.dimensions.sub-query.md +2 -2
  9. package/dist/docs/topics/cubes.dimensions.time.md +2 -2
  10. package/dist/docs/topics/cubes.dimensions.types.md +2 -2
  11. package/dist/docs/topics/cubes.extends.md +2 -2
  12. package/dist/docs/topics/cubes.hierarchies.md +2 -2
  13. package/dist/docs/topics/cubes.joins.md +2 -2
  14. package/dist/docs/topics/cubes.md +2 -2
  15. package/dist/docs/topics/cubes.measures.calculated.md +2 -2
  16. package/dist/docs/topics/cubes.measures.drill-members.md +2 -2
  17. package/dist/docs/topics/cubes.measures.filters.md +2 -2
  18. package/dist/docs/topics/cubes.measures.format.md +21 -2
  19. package/dist/docs/topics/cubes.measures.md +2 -2
  20. package/dist/docs/topics/cubes.measures.rolling.md +2 -2
  21. package/dist/docs/topics/cubes.measures.types.md +2 -2
  22. package/dist/docs/topics/cubes.public.md +2 -2
  23. package/dist/docs/topics/cubes.refresh-key.md +2 -2
  24. package/dist/docs/topics/cubes.segments.md +2 -2
  25. package/dist/docs/topics/cubes.sql.md +2 -2
  26. package/dist/docs/topics/features.catalog.md +31 -0
  27. package/dist/docs/topics/features.cli.md +59 -0
  28. package/dist/docs/topics/features.context-graph.md +18 -0
  29. package/dist/docs/topics/features.governance.md +84 -0
  30. package/dist/docs/topics/features.mcp.md +48 -0
  31. package/dist/docs/topics/features.md +15 -0
  32. package/dist/docs/topics/features.sdk.md +53 -0
  33. package/dist/docs/topics/features.semantic-layer.md +50 -0
  34. package/dist/docs/topics/features.slack-teams.md +18 -0
  35. package/dist/docs/topics/getting-started.md +2 -143
  36. package/dist/docs/topics/pre-aggregations.md +2 -2
  37. package/dist/docs/topics/pre-aggregations.rollup.md +2 -2
  38. package/dist/docs/topics/syntax.context-variables.md +2 -2
  39. package/dist/docs/topics/syntax.md +2 -2
  40. package/dist/docs/topics/syntax.references.md +2 -2
  41. package/dist/docs/topics/views.cubes.md +2 -2
  42. package/dist/docs/topics/views.folders.md +2 -2
  43. package/dist/docs/topics/views.includes.md +2 -2
  44. package/dist/docs/topics/views.md +2 -2
  45. package/dist/docs/topics/workflow.deploy.md +79 -14
  46. package/dist/docs/topics/workflow.mcp.md +19 -13
  47. package/dist/docs/topics/workflow.md +25 -8
  48. package/dist/docs/topics/workflow.query.md +2 -2
  49. package/dist/docs/topics/workflow.validate.md +4 -31
  50. package/dist/templates/claude/skills/bonnard-get-started/SKILL.md +16 -26
  51. package/dist/templates/cursor/rules/bonnard-get-started.mdc +16 -26
  52. package/dist/templates/shared/bonnard.md +31 -6
  53. package/package.json +4 -8
  54. package/dist/bin/validate-DiN3DaTl.mjs +0 -110
  55. /package/dist/bin/{cubes-De1_2_YJ.mjs → cubes-Bf0IPYd7.mjs} +0 -0
@@ -0,0 +1,365 @@
1
+ import { t as getProjectPaths } from "./bon.mjs";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import YAML from "yaml";
5
+ import { z } from "zod";
6
+
7
+ //#region src/lib/schema.ts
8
+ const identifier = z.string().regex(/^[_a-zA-Z][_a-zA-Z0-9]*$/, "must be a valid identifier (letters, numbers, underscores; cannot start with a number)");
9
+ const refreshKeySchema = z.object({
10
+ every: z.string().regex(/^\d+\s+(second|minute|hour|day|week)s?$/, { message: "must be a time interval like \"1 hour\", \"30 minute\", \"1 day\"" }).optional(),
11
+ sql: z.string().optional()
12
+ });
13
+ const measureTypes = [
14
+ "count",
15
+ "count_distinct",
16
+ "count_distinct_approx",
17
+ "sum",
18
+ "avg",
19
+ "min",
20
+ "max",
21
+ "number",
22
+ "string",
23
+ "time",
24
+ "boolean",
25
+ "running_total",
26
+ "number_agg"
27
+ ];
28
+ const dimensionTypes = [
29
+ "string",
30
+ "number",
31
+ "boolean",
32
+ "time",
33
+ "geo",
34
+ "switch"
35
+ ];
36
+ const relationshipTypes = [
37
+ "many_to_one",
38
+ "one_to_many",
39
+ "one_to_one"
40
+ ];
41
+ const granularities = [
42
+ "second",
43
+ "minute",
44
+ "hour",
45
+ "day",
46
+ "week",
47
+ "month",
48
+ "quarter",
49
+ "year"
50
+ ];
51
+ const preAggTypes = [
52
+ "rollup",
53
+ "original_sql",
54
+ "rollup_join",
55
+ "rollup_lambda"
56
+ ];
57
+ const formats = [
58
+ "percent",
59
+ "currency",
60
+ "number",
61
+ "imageUrl",
62
+ "link",
63
+ "id"
64
+ ];
65
+ const measureSchema = z.object({
66
+ name: identifier,
67
+ type: z.enum(measureTypes),
68
+ sql: z.string().optional(),
69
+ description: z.string().optional(),
70
+ title: z.string().optional(),
71
+ format: z.enum(formats).optional(),
72
+ public: z.boolean().optional(),
73
+ filters: z.array(z.object({ sql: z.string() })).optional(),
74
+ rolling_window: z.object({
75
+ trailing: z.string().optional(),
76
+ leading: z.string().optional(),
77
+ offset: z.string().optional()
78
+ }).optional(),
79
+ drill_members: z.array(z.string()).optional(),
80
+ meta: z.record(z.string(), z.unknown()).optional()
81
+ });
82
+ const dimensionFormatSchema = z.union([z.enum(formats), z.string().startsWith("%")]);
83
+ const dimensionSchema = z.object({
84
+ name: identifier,
85
+ type: z.enum(dimensionTypes),
86
+ sql: z.string().optional(),
87
+ primary_key: z.boolean().optional(),
88
+ sub_query: z.boolean().optional(),
89
+ propagate_filters_to_sub_query: z.boolean().optional(),
90
+ description: z.string().optional(),
91
+ title: z.string().optional(),
92
+ format: dimensionFormatSchema.optional(),
93
+ public: z.boolean().optional(),
94
+ meta: z.record(z.string(), z.unknown()).optional(),
95
+ latitude: z.object({ sql: z.string() }).optional(),
96
+ longitude: z.object({ sql: z.string() }).optional(),
97
+ case: z.object({
98
+ when: z.array(z.object({
99
+ sql: z.string(),
100
+ label: z.string()
101
+ })),
102
+ else: z.object({ label: z.string() }).optional()
103
+ }).optional()
104
+ });
105
+ const joinSchema = z.object({
106
+ name: identifier,
107
+ relationship: z.enum(relationshipTypes),
108
+ sql: z.string()
109
+ });
110
+ const segmentSchema = z.object({
111
+ name: identifier,
112
+ sql: z.string(),
113
+ description: z.string().optional(),
114
+ title: z.string().optional(),
115
+ public: z.boolean().optional()
116
+ });
117
+ const preAggregationSchema = z.object({
118
+ name: identifier,
119
+ type: z.enum(preAggTypes).optional(),
120
+ measures: z.array(z.string()).optional(),
121
+ dimensions: z.array(z.string()).optional(),
122
+ time_dimension: z.string().optional(),
123
+ granularity: z.enum(granularities).optional(),
124
+ partition_granularity: z.enum(granularities).optional(),
125
+ refresh_key: refreshKeySchema.optional(),
126
+ scheduled_refresh: z.boolean().optional()
127
+ });
128
+ const hierarchySchema = z.object({
129
+ name: identifier,
130
+ levels: z.array(z.string()),
131
+ title: z.string().optional(),
132
+ public: z.boolean().optional()
133
+ });
134
+ const cubeSchema = z.object({
135
+ name: identifier,
136
+ sql: z.string().optional(),
137
+ sql_table: z.string().optional(),
138
+ data_source: z.string().optional(),
139
+ extends: z.string().optional(),
140
+ description: z.string().optional(),
141
+ title: z.string().optional(),
142
+ public: z.boolean().optional(),
143
+ refresh_key: refreshKeySchema.optional(),
144
+ measures: z.array(measureSchema).optional(),
145
+ dimensions: z.array(dimensionSchema).optional(),
146
+ joins: z.array(joinSchema).optional(),
147
+ segments: z.array(segmentSchema).optional(),
148
+ pre_aggregations: z.array(preAggregationSchema).optional(),
149
+ hierarchies: z.array(hierarchySchema).optional()
150
+ }).refine((data) => data.sql != null || data.sql_table != null || data.extends != null, { message: "sql, sql_table, or extends is required" });
151
+ const viewIncludeItemSchema = z.union([z.string(), z.object({
152
+ name: z.string(),
153
+ alias: z.string().optional(),
154
+ title: z.string().optional(),
155
+ description: z.string().optional(),
156
+ format: z.string().optional(),
157
+ meta: z.record(z.string(), z.unknown()).optional()
158
+ })]);
159
+ const viewCubeRefSchema = z.object({
160
+ join_path: z.string(),
161
+ includes: z.union([z.literal("*"), z.array(viewIncludeItemSchema)]).optional(),
162
+ excludes: z.array(z.string()).optional(),
163
+ prefix: z.boolean().optional()
164
+ });
165
+ const folderSchema = z.lazy(() => z.object({
166
+ name: z.string(),
167
+ members: z.array(z.string()).optional(),
168
+ folders: z.array(folderSchema).optional()
169
+ }));
170
+ const viewMeasureSchema = z.object({
171
+ name: identifier,
172
+ sql: z.string(),
173
+ type: z.string(),
174
+ format: z.string().optional(),
175
+ description: z.string().optional(),
176
+ meta: z.record(z.string(), z.unknown()).optional()
177
+ });
178
+ const viewDimensionSchema = z.object({
179
+ name: identifier,
180
+ sql: z.string(),
181
+ type: z.string(),
182
+ description: z.string().optional(),
183
+ meta: z.record(z.string(), z.unknown()).optional()
184
+ });
185
+ const viewSegmentSchema = z.object({
186
+ name: identifier,
187
+ sql: z.string(),
188
+ description: z.string().optional()
189
+ });
190
+ const viewSchema = z.object({
191
+ name: identifier,
192
+ description: z.string().optional(),
193
+ title: z.string().optional(),
194
+ public: z.boolean().optional(),
195
+ cubes: z.array(viewCubeRefSchema).optional(),
196
+ measures: z.array(viewMeasureSchema).optional(),
197
+ dimensions: z.array(viewDimensionSchema).optional(),
198
+ segments: z.array(viewSegmentSchema).optional(),
199
+ folders: z.array(folderSchema).optional()
200
+ });
201
+ const fileSchema = z.object({
202
+ cubes: z.array(cubeSchema).optional(),
203
+ views: z.array(viewSchema).optional()
204
+ }).refine((data) => data.cubes && data.cubes.length > 0 || data.views && data.views.length > 0, "File must contain at least one cube or view");
205
+ function formatZodError(error, fileName, parsed) {
206
+ return error.issues.map((issue) => {
207
+ const pathParts = issue.path;
208
+ let entityContext = "";
209
+ if (pathParts.length >= 2) {
210
+ const collection = pathParts[0];
211
+ const index = pathParts[1];
212
+ const entity = parsed?.[collection]?.[index];
213
+ if (entity?.name) entityContext = ` (${entity.name})`;
214
+ }
215
+ const pathStr = pathParts.join(".");
216
+ const location = pathStr ? `${pathStr}${entityContext}` : "";
217
+ if (issue.code === "invalid_value" && "values" in issue) return `${fileName}: ${location} — invalid value, expected one of: ${issue.values.join(", ")}`;
218
+ return `${fileName}: ${location ? `${location} — ` : ""}${issue.message}`;
219
+ });
220
+ }
221
+ function validateFiles(files) {
222
+ const errors = [];
223
+ const cubes = [];
224
+ const views = [];
225
+ const allNames = /* @__PURE__ */ new Map();
226
+ for (const file of files) {
227
+ let parsed;
228
+ try {
229
+ parsed = YAML.parse(file.content);
230
+ } catch (err) {
231
+ errors.push(`${file.fileName}: YAML parse error — ${err.message}`);
232
+ continue;
233
+ }
234
+ if (!parsed || typeof parsed !== "object") {
235
+ errors.push(`${file.fileName}: file is empty or not a YAML object`);
236
+ continue;
237
+ }
238
+ const result = fileSchema.safeParse(parsed);
239
+ if (!result.success) {
240
+ errors.push(...formatZodError(result.error, file.fileName, parsed));
241
+ continue;
242
+ }
243
+ for (const cube of parsed.cubes ?? []) if (cube.name) {
244
+ const existing = allNames.get(cube.name);
245
+ if (existing) errors.push(`${file.fileName}: duplicate name '${cube.name}' (also defined in ${existing})`);
246
+ else {
247
+ allNames.set(cube.name, file.fileName);
248
+ cubes.push(cube.name);
249
+ }
250
+ }
251
+ for (const view of parsed.views ?? []) if (view.name) {
252
+ const existing = allNames.get(view.name);
253
+ if (existing) errors.push(`${file.fileName}: duplicate name '${view.name}' (also defined in ${existing})`);
254
+ else {
255
+ allNames.set(view.name, file.fileName);
256
+ views.push(view.name);
257
+ }
258
+ }
259
+ }
260
+ return {
261
+ errors,
262
+ cubes,
263
+ views
264
+ };
265
+ }
266
+
267
+ //#endregion
268
+ //#region src/lib/validate.ts
269
+ function collectYamlFiles(dir, rootDir) {
270
+ if (!fs.existsSync(dir)) return [];
271
+ const results = [];
272
+ function walk(current) {
273
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
274
+ const fullPath = path.join(current, entry.name);
275
+ if (entry.isDirectory()) walk(fullPath);
276
+ else if (entry.name.endsWith(".yaml") || entry.name.endsWith(".yml")) results.push({
277
+ fileName: path.relative(rootDir, fullPath),
278
+ content: fs.readFileSync(fullPath, "utf-8")
279
+ });
280
+ }
281
+ }
282
+ walk(dir);
283
+ return results;
284
+ }
285
+ function checkMissingDescriptions(files) {
286
+ const missing = [];
287
+ for (const file of files) try {
288
+ const parsed = YAML.parse(file.content);
289
+ if (!parsed) continue;
290
+ const cubes = parsed.cubes || [];
291
+ for (const cube of cubes) {
292
+ if (!cube.name) continue;
293
+ if (!cube.description) missing.push({
294
+ parent: cube.name,
295
+ type: "cube",
296
+ name: cube.name
297
+ });
298
+ const measures = cube.measures || [];
299
+ for (const measure of measures) if (measure.name && !measure.description) missing.push({
300
+ parent: cube.name,
301
+ type: "measure",
302
+ name: measure.name
303
+ });
304
+ const dimensions = cube.dimensions || [];
305
+ for (const dimension of dimensions) if (dimension.name && !dimension.description) missing.push({
306
+ parent: cube.name,
307
+ type: "dimension",
308
+ name: dimension.name
309
+ });
310
+ }
311
+ const views = parsed.views || [];
312
+ for (const view of views) {
313
+ if (!view.name) continue;
314
+ if (!view.description) missing.push({
315
+ parent: view.name,
316
+ type: "view",
317
+ name: view.name
318
+ });
319
+ }
320
+ } catch {}
321
+ return missing;
322
+ }
323
+ function checkMissingDataSource(files) {
324
+ const missing = [];
325
+ for (const file of files) try {
326
+ const parsed = YAML.parse(file.content);
327
+ if (!parsed) continue;
328
+ for (const cube of parsed.cubes || []) if (cube.name && !cube.data_source) missing.push(cube.name);
329
+ } catch {}
330
+ return missing;
331
+ }
332
+ async function validate(projectPath) {
333
+ const paths = getProjectPaths(projectPath);
334
+ const files = [...collectYamlFiles(paths.cubes, projectPath), ...collectYamlFiles(paths.views, projectPath)];
335
+ if (files.length === 0) return {
336
+ valid: true,
337
+ errors: [],
338
+ cubes: [],
339
+ views: [],
340
+ missingDescriptions: [],
341
+ cubesMissingDataSource: []
342
+ };
343
+ const result = validateFiles(files);
344
+ if (result.errors.length > 0) return {
345
+ valid: false,
346
+ errors: result.errors,
347
+ cubes: [],
348
+ views: [],
349
+ missingDescriptions: [],
350
+ cubesMissingDataSource: []
351
+ };
352
+ const missingDescriptions = checkMissingDescriptions(files);
353
+ const cubesMissingDataSource = checkMissingDataSource(files);
354
+ return {
355
+ valid: true,
356
+ errors: [],
357
+ cubes: result.cubes,
358
+ views: result.views,
359
+ missingDescriptions,
360
+ cubesMissingDataSource
361
+ };
362
+ }
363
+
364
+ //#endregion
365
+ export { validate };
@@ -1,6 +1,6 @@
1
1
  # Bonnard Documentation
2
2
 
3
- > Build semantic layers with cubes and views.
3
+ > Learn how to build, deploy, and query a semantic layer with Bonnard. Define metrics once in YAML cubes and views, then query from any BI tool or AI agent.
4
4
 
5
5
  ## Cubes
6
6
 
@@ -1,6 +1,6 @@
1
- # cubes.data-source
1
+ # Data Source
2
2
 
3
- > Connect cubes to specific data warehouses.
3
+ > The data_source property connects cubes to specific data warehouse connections. Use it when your semantic layer spans multiple databases like PostgreSQL, Snowflake, or BigQuery.
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
- # cubes.dimensions.format
1
+ # Dimension Format
2
2
 
3
- > Control how dimension values are displayed.
3
+ > The format property controls how dimension values are displayed to consumers. Apply date formatting, number formatting, or custom patterns to make dimension output human-readable.
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
- # cubes.dimensions
1
+ # Dimensions
2
2
 
3
- > Define attributes for grouping and filtering data.
3
+ > Dimensions define the attributes used for grouping and filtering data in your semantic layer. They map to table columns and provide the axes for slicing measures in queries.
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
- # cubes.dimensions.primary-key
1
+ # Primary Key
2
2
 
3
- > Mark the unique identifier dimension for a cube.
3
+ > The primary_key property marks a dimension as the unique identifier for a cube. Primary keys are required for count_distinct measures and for establishing correct join relationships.
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
- # cubes.dimensions.sub-query
1
+ # Sub-Query Dimensions
2
2
 
3
- > Bring measures from other cubes into a dimension.
3
+ > Sub-query dimensions let you bring a measure from another cube into a dimension using a correlated subquery. Useful for denormalizing aggregated values like "lifetime revenue per user."
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
- # cubes.dimensions.time
1
+ # Time Dimensions
2
2
 
3
- > Enable time-based analysis with time dimensions.
3
+ > Time dimensions enable date and time-based analysis in your semantic layer. They support automatic granularity (day, week, month, year) and power time-series queries and date filters.
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
- # cubes.dimensions.types
1
+ # Dimension Types
2
2
 
3
- > The 6 dimension types for categorizing and filtering data.
3
+ > Reference for all 6 dimension types in Bonnard: string, number, boolean, time, geo, and json. Each type determines how the dimension is stored, indexed, and queried.
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
- # cubes.extends
1
+ # Extends
2
2
 
3
- > Reuse members from other cubes to reduce duplication.
3
+ > Extends lets you inherit measures, dimensions, and joins from other cubes to reduce duplication. Build base cubes with shared logic and extend them for specific use cases.
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
- # cubes.hierarchies
1
+ # Hierarchies
2
2
 
3
- > Define drill-down paths for dimensional analysis.
3
+ > Hierarchies define drill-down paths for dimensional analysis in your semantic layer. Set up parent-child relationships between dimensions so consumers can explore data at different levels.
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
- # cubes.joins
1
+ # Joins
2
2
 
3
- > Connect cubes together to enable cross-cube analysis.
3
+ > Joins connect cubes together so you can query measures and dimensions across multiple tables. Define one-to-many, many-to-one, and one-to-one relationships between cubes.
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
- # cubes
1
+ # Cubes
2
2
 
3
- > Define cubes that map to your database tables.
3
+ > Cubes are the core building blocks of a Bonnard semantic layer. Each cube maps to a database table and defines the measures, dimensions, and joins available for querying.
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
- # cubes.measures.calculated
1
+ # Calculated Measures
2
2
 
3
- > Build complex metrics from other measures.
3
+ > Calculated measures let you build complex metrics from other measures in the same cube. Combine existing aggregations to create ratios, percentages, and derived metrics without raw SQL.
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
- # cubes.measures.drill-members
1
+ # Drill Members
2
2
 
3
- > Define which dimensions to show when drilling into a measure.
3
+ > Drill members define which dimensions are shown when a user drills into a measure. Configure drill-down paths so consumers can explore aggregated numbers down to individual records.
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
- # cubes.measures.filters
1
+ # Measure Filters
2
2
 
3
- > Apply permanent filters to measures for conditional aggregations.
3
+ > Measure filters apply permanent WHERE conditions to measures for conditional aggregations. Create filtered metrics like "revenue from paid plans" or "active users in the last 30 days."
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
- # cubes.measures.format
1
+ # Measure Format
2
2
 
3
- > Specify how measure values should be displayed.
3
+ > The format property controls how measure values are displayed to consumers. Specify currency symbols, decimal places, percentage formatting, and custom number patterns.
4
4
 
5
5
  ## Overview
6
6
 
@@ -27,6 +27,12 @@ measures:
27
27
 
28
28
  ## Supported Formats
29
29
 
30
+ | Format | Description |
31
+ |--------|-------------|
32
+ | `percent` | Percentage display (0.75 → 75%) |
33
+ | `currency` | Monetary display ($1,234.56) |
34
+ | `number` | Plain number with standard formatting |
35
+
30
36
  ### percent
31
37
 
32
38
  Displays value as a percentage:
@@ -53,6 +59,19 @@ Displays value as monetary amount:
53
59
 
54
60
  Output: `$1,234.56` (formatting depends on BI tool locale)
55
61
 
62
+ ### number
63
+
64
+ Standard number formatting with grouping separators:
65
+
66
+ ```yaml
67
+ - name: total_slots
68
+ type: sum
69
+ sql: slot_count
70
+ format: number
71
+ ```
72
+
73
+ Output: `1,234` instead of raw `1234`. Use this when the measure represents a count or quantity that benefits from thousand separators.
74
+
56
75
  ## Usage Notes
57
76
 
58
77
  ### Format vs Calculation
@@ -1,6 +1,6 @@
1
- # cubes.measures
1
+ # Measures
2
2
 
3
- > Define metrics and aggregations for analytical queries.
3
+ > Measures define the metrics and aggregations in your semantic layer. Use them to calculate sums, counts, averages, and custom expressions that stay consistent across every consumer.
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
- # cubes.measures.rolling
1
+ # Rolling Measures
2
2
 
3
- > Calculate rolling window aggregations (7-day average, 30-day sum, etc).
3
+ > Rolling measures calculate aggregations over sliding time windows like 7-day averages, 30-day sums, and month-to-date totals. Define the window type, trailing period, and offset.
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
- # cubes.measures.types
1
+ # Measure Types
2
2
 
3
- > The 12 measure types available for aggregating data.
3
+ > Reference for all 12 measure types available in Bonnard: count, sum, avg, min, max, count_distinct, running_total, and more. Each type maps to a specific SQL aggregation.
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
- # cubes.public
1
+ # Public
2
2
 
3
- > Control visibility of cubes, measures, and dimensions in the API.
3
+ > The public property controls whether cubes, measures, and dimensions are exposed in the API. Set public to false to hide internal implementation details from consumers.
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
- # cubes.refresh-key
1
+ # Refresh Key
2
2
 
3
- > Control when cube data is refreshed in the cache.
3
+ > The refresh_key property controls when cube data is refreshed in the cache. Define time-based or query-based refresh strategies to balance data freshness with query performance.
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
- # cubes.segments
1
+ # Segments
2
2
 
3
- > Define reusable row-level filters.
3
+ > Segments define reusable row-level filters that can be applied to any query on a cube. Use them for common filters like "active users" or "paid orders" that multiple consumers need.
4
4
 
5
5
  ## Overview
6
6
 
@@ -1,6 +1,6 @@
1
- # cubes.sql
1
+ # SQL
2
2
 
3
- > Define the SQL table or subquery that powers a cube.
3
+ > Define the SQL table or subquery that powers a cube. Use the sql property to point to a physical table, or write a SELECT statement for derived datasets and transformations.
4
4
 
5
5
  ## Overview
6
6
 
@@ -0,0 +1,31 @@
1
+ # Catalog
2
+
3
+ > Browse and understand your data model — no code required.
4
+
5
+ The Bonnard catalog gives everyone on your team a live view of your semantic layer. Browse cubes, views, measures, and dimensions from the browser. Understand what data is available before writing a single query.
6
+
7
+ ## What you can explore
8
+
9
+ - **Cubes and Views** — See every deployed source with field counts at a glance
10
+ - **Measures** — Aggregation type, SQL expression, format (currency, percentage), and description
11
+ - **Dimensions** — Data type, time granularity options, and custom metadata
12
+ - **Segments** — Pre-defined filters available for queries
13
+
14
+ ## Field-level detail
15
+
16
+ Click any field to see exactly how it's calculated:
17
+
18
+ - **SQL expression** — The underlying query logic
19
+ - **Type and format** — How the field is aggregated and displayed
20
+ - **Origin cube** — Which cube a view field traces back to
21
+ - **Referenced fields** — Dependencies this field relies on
22
+ - **Custom metadata** — Tags, labels, and annotations set by your data team
23
+
24
+ ## Built for business users
25
+
26
+ The catalog is designed for anyone who needs to understand the data, not just engineers. No YAML, no terminal, no warehouse credentials. Browse the schema, read descriptions, and know exactly what to ask your AI agent for.
27
+
28
+ ## See Also
29
+
30
+ - [views](views) — How to create curated views for your team
31
+ - [cubes.public](cubes.public) — Control which cubes are visible