@bonnard/cli 0.1.13 → 0.2.0

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 (53) hide show
  1. package/dist/bin/bon.mjs +249 -5
  2. package/dist/bin/{validate-DiN3DaTl.mjs → validate-C31hmPk8.mjs} +15 -3
  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 +2 -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 +60 -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 -5
  48. package/dist/docs/topics/workflow.query.md +2 -2
  49. package/dist/docs/topics/workflow.validate.md +2 -2
  50. package/dist/templates/claude/skills/bonnard-get-started/SKILL.md +12 -3
  51. package/dist/templates/cursor/rules/bonnard-get-started.mdc +12 -3
  52. package/dist/templates/shared/bonnard.md +31 -4
  53. package/package.json +1 -1
package/dist/bin/bon.mjs CHANGED
@@ -2279,7 +2279,7 @@ async function validateCommand(options = {}) {
2279
2279
  console.log(pc.red("No bon.yaml found. Are you in a Bonnard project?"));
2280
2280
  process.exit(1);
2281
2281
  }
2282
- const { validate } = await import("./validate-DiN3DaTl.mjs");
2282
+ const { validate } = await import("./validate-C31hmPk8.mjs");
2283
2283
  const result = await validate(cwd);
2284
2284
  if (result.cubes.length === 0 && result.views.length === 0 && result.valid) {
2285
2285
  console.log(pc.yellow(`No cube or view files found in ${BONNARD_DIR}/cubes/ or ${BONNARD_DIR}/views/.`));
@@ -2307,6 +2307,13 @@ async function validateCommand(options = {}) {
2307
2307
  }
2308
2308
  for (const [parent, items] of byParent) console.log(pc.dim(` ${parent}: ${items.join(", ")}`));
2309
2309
  }
2310
+ if (result.cubesMissingDataSource.length > 0) {
2311
+ console.log();
2312
+ console.log(pc.yellow(`⚠ ${result.cubesMissingDataSource.length} cube(s) missing data_source`));
2313
+ console.log(pc.dim(" Without an explicit data_source, cubes use the default warehouse."));
2314
+ console.log(pc.dim(" This can cause issues when multiple warehouses are configured."));
2315
+ console.log(pc.dim(` ${result.cubesMissingDataSource.join(", ")}`));
2316
+ }
2310
2317
  if (options.testConnection) {
2311
2318
  console.log();
2312
2319
  await testReferencedConnections(cwd);
@@ -2383,6 +2390,9 @@ function collectFiles(dir, rootDir) {
2383
2390
  walk(dir);
2384
2391
  return files;
2385
2392
  }
2393
+ function capitalize$1(s) {
2394
+ return s.charAt(0).toUpperCase() + s.slice(1);
2395
+ }
2386
2396
  async function deployCommand(options = {}) {
2387
2397
  const cwd = process.cwd();
2388
2398
  const paths = getProjectPaths(cwd);
@@ -2391,7 +2401,7 @@ async function deployCommand(options = {}) {
2391
2401
  process.exit(1);
2392
2402
  }
2393
2403
  console.log(pc.dim("Validating cubes and views..."));
2394
- const { validate } = await import("./validate-DiN3DaTl.mjs");
2404
+ const { validate } = await import("./validate-C31hmPk8.mjs");
2395
2405
  const result = await validate(cwd);
2396
2406
  if (!result.valid) {
2397
2407
  console.log(pc.red("Validation failed:\n"));
@@ -2412,10 +2422,40 @@ async function deployCommand(options = {}) {
2412
2422
  console.log(pc.dim(`Deploying ${fileCount} file(s)...`));
2413
2423
  console.log();
2414
2424
  try {
2415
- const response = await post("/api/deploy", { files });
2425
+ const response = await post("/api/deploy", {
2426
+ files,
2427
+ ...options.message && { message: options.message }
2428
+ });
2416
2429
  console.log(pc.green("Deploy successful!"));
2417
2430
  console.log(`Deployment ID: ${pc.cyan(response.deployment.id)}`);
2418
- console.log(`API: ${pc.cyan(response.deployment.cubeApiUrl)}`);
2431
+ console.log();
2432
+ if (response.deployment.isFirstDeploy) console.log(pc.dim(` First deployment — ${response.deployment.fileCount} files uploaded`));
2433
+ else if (response.deployment.changes && response.deployment.changes.details.length > 0) {
2434
+ const { changes } = response.deployment;
2435
+ console.log(pc.dim("Changes from previous deploy:"));
2436
+ console.log();
2437
+ for (const c of changes.details) {
2438
+ const prefix = c.changeType === "added" ? pc.green("+") : c.changeType === "removed" ? pc.red("-") : pc.yellow("~");
2439
+ const label = `${capitalize$1(c.changeType)} ${c.objectType}: ${c.objectName}`;
2440
+ const breakingTag = c.breaking ? pc.red(" BREAKING") : "";
2441
+ const summaryTag = c.summary ? pc.dim(` — ${c.summary}`) : "";
2442
+ console.log(` ${prefix} ${label}${summaryTag}${breakingTag}`);
2443
+ }
2444
+ if (changes.breaking > 0) {
2445
+ console.log();
2446
+ console.log(pc.red(`${changes.breaking} breaking change${changes.breaking > 1 ? "s" : ""} detected`));
2447
+ }
2448
+ console.log();
2449
+ console.log(pc.bold("Annotate these changes with reasoning:"));
2450
+ console.log();
2451
+ const annotationTemplate = { annotations: changes.details.map((c) => ({
2452
+ objectName: c.objectName,
2453
+ annotation: `Why ${c.objectName} was ${c.changeType}`
2454
+ })) };
2455
+ console.log(` bon annotate ${response.deployment.id} --data '${JSON.stringify(annotationTemplate)}'`);
2456
+ console.log();
2457
+ console.log(pc.dim("Replace each annotation value with the reasoning behind the change."));
2458
+ } else console.log(pc.dim(" No changes detected from previous deployment."));
2419
2459
  console.log();
2420
2460
  console.log(pc.bold("Connect AI agents via MCP:"));
2421
2461
  console.log(` MCP URL: ${pc.cyan("https://mcp.bonnard.dev/mcp")}`);
@@ -2532,6 +2572,207 @@ async function testAndSyncDatasources(cwd, options = {}) {
2532
2572
  return false;
2533
2573
  }
2534
2574
 
2575
+ //#endregion
2576
+ //#region src/commands/deployments.ts
2577
+ function relativeTime(dateStr) {
2578
+ const now = Date.now();
2579
+ const then = new Date(dateStr).getTime();
2580
+ const seconds = Math.floor((now - then) / 1e3);
2581
+ if (seconds < 60) return "just now";
2582
+ const minutes = Math.floor(seconds / 60);
2583
+ if (minutes < 60) return `${minutes} min ago`;
2584
+ const hours = Math.floor(minutes / 60);
2585
+ if (hours < 24) return `${hours} hour${hours > 1 ? "s" : ""} ago`;
2586
+ const days = Math.floor(hours / 24);
2587
+ return `${days} day${days > 1 ? "s" : ""} ago`;
2588
+ }
2589
+ function truncate(str, max) {
2590
+ if (str.length <= max) return str;
2591
+ return str.slice(0, max - 1) + "…";
2592
+ }
2593
+ function statusColor(status) {
2594
+ switch (status) {
2595
+ case "success": return pc.green(status);
2596
+ case "failed": return pc.red(status);
2597
+ case "processing": return pc.yellow(status);
2598
+ default: return pc.dim(status);
2599
+ }
2600
+ }
2601
+ async function deploymentsCommand(options = {}) {
2602
+ const limit = options.all ? 100 : 10;
2603
+ let response;
2604
+ try {
2605
+ response = await get(`/api/deploy/history?limit=${limit}`);
2606
+ } catch (err) {
2607
+ console.log(pc.red(`Failed to fetch deployments: ${err instanceof Error ? err.message : err}`));
2608
+ process.exit(1);
2609
+ }
2610
+ const { deployments } = response;
2611
+ if (options.format === "json") {
2612
+ console.log(JSON.stringify(deployments, null, 2));
2613
+ return;
2614
+ }
2615
+ console.log();
2616
+ console.log(pc.bold("Deployments for Bonnard"));
2617
+ console.log();
2618
+ if (deployments.length === 0) {
2619
+ console.log(pc.dim(" No deployments found."));
2620
+ console.log();
2621
+ return;
2622
+ }
2623
+ const colId = 10;
2624
+ const colStatus = 12;
2625
+ const colFiles = 7;
2626
+ const colMessage = 32;
2627
+ console.log(pc.dim(" " + "ID".padEnd(colId) + "Status".padEnd(colStatus) + "Files".padEnd(colFiles) + "Message".padEnd(colMessage) + "Deployed"));
2628
+ for (const d of deployments) {
2629
+ const id = d.id.slice(0, 8);
2630
+ const status = statusColor(d.status);
2631
+ const statusPad = " ".repeat(Math.max(0, colStatus - d.status.length));
2632
+ const files = String(d.fileCount);
2633
+ const message = d.message ? truncate(d.message, 30) : "—";
2634
+ const time = relativeTime(d.createdAt);
2635
+ console.log(" " + id.padEnd(colId) + status + statusPad + files.padEnd(colFiles) + message.padEnd(colMessage) + pc.dim(time));
2636
+ }
2637
+ console.log();
2638
+ console.log(pc.dim(`Showing ${deployments.length} deployment${deployments.length !== 1 ? "s" : ""}.`));
2639
+ console.log();
2640
+ }
2641
+
2642
+ //#endregion
2643
+ //#region src/commands/annotate.ts
2644
+ function readStdin() {
2645
+ return new Promise((resolve) => {
2646
+ if (process.stdin.isTTY) {
2647
+ resolve(null);
2648
+ return;
2649
+ }
2650
+ let data = "";
2651
+ process.stdin.setEncoding("utf8");
2652
+ process.stdin.on("data", (chunk) => data += chunk);
2653
+ process.stdin.on("end", () => resolve(data.trim() || null));
2654
+ setTimeout(() => resolve(data.trim() || null), 100);
2655
+ });
2656
+ }
2657
+ function parseAnnotations(raw) {
2658
+ let parsed;
2659
+ try {
2660
+ parsed = JSON.parse(raw);
2661
+ } catch {
2662
+ throw new Error("Invalid JSON. Expected: {\"annotations\": [{\"objectName\": \"...\", \"annotation\": \"...\"}]}");
2663
+ }
2664
+ const obj = parsed;
2665
+ if (!obj.annotations || !Array.isArray(obj.annotations) || obj.annotations.length === 0) throw new Error("JSON must contain a non-empty \"annotations\" array");
2666
+ for (const entry of obj.annotations) {
2667
+ const e = entry;
2668
+ if (!e.objectName || typeof e.objectName !== "string") throw new Error("Each annotation must have a string \"objectName\"");
2669
+ if (!e.annotation || typeof e.annotation !== "string") throw new Error(`Missing \"annotation\" text for \"${e.objectName}\"`);
2670
+ if (e.annotation.length > 1e3) throw new Error(`Annotation for \"${e.objectName}\" exceeds 1000 chars`);
2671
+ }
2672
+ return { annotations: obj.annotations };
2673
+ }
2674
+ async function annotateCommand(id, options = {}) {
2675
+ const raw = options.data || await readStdin();
2676
+ if (!raw) {
2677
+ try {
2678
+ const { changes } = await get(`/api/deploy/changes/${id}`);
2679
+ if (changes.length === 0) {
2680
+ console.log(pc.dim("No changes recorded for this deployment."));
2681
+ return;
2682
+ }
2683
+ console.log();
2684
+ console.log(pc.bold(`Changes in deployment ${id.slice(0, 8)}`));
2685
+ console.log();
2686
+ for (const c of changes) {
2687
+ const prefix = c.changeType === "added" ? pc.green("+") : c.changeType === "removed" ? pc.red("-") : pc.yellow("~");
2688
+ const label = `${c.changeType} ${c.objectType}: ${c.objectName}`;
2689
+ const breakingTag = c.breaking ? pc.red(" BREAKING") : "";
2690
+ console.log(` ${prefix} ${label}${breakingTag}`);
2691
+ if (c.annotation) console.log(pc.dim(` "${c.annotation}"`));
2692
+ }
2693
+ console.log();
2694
+ console.log(pc.dim("To annotate, provide JSON via --data or stdin:"));
2695
+ console.log(pc.dim(` bon annotate ${id.slice(0, 8)} --data '{"annotations":[{"objectName":"...","annotation":"..."}]}'`));
2696
+ console.log();
2697
+ } catch (err) {
2698
+ console.log(pc.red(`Failed to fetch changes: ${err instanceof Error ? err.message : err}`));
2699
+ process.exit(1);
2700
+ }
2701
+ return;
2702
+ }
2703
+ let payload;
2704
+ try {
2705
+ payload = parseAnnotations(raw);
2706
+ } catch (err) {
2707
+ console.log(pc.red(err instanceof Error ? err.message : String(err)));
2708
+ process.exit(1);
2709
+ }
2710
+ try {
2711
+ const response = await post(`/api/deploy/annotate/${id}`, payload);
2712
+ console.log(pc.green(`Annotated ${response.updated}/${response.total} changes.`));
2713
+ } catch (err) {
2714
+ console.log(pc.red(`Failed to submit annotations: ${err instanceof Error ? err.message : err}`));
2715
+ process.exit(1);
2716
+ }
2717
+ }
2718
+
2719
+ //#endregion
2720
+ //#region src/commands/diff.ts
2721
+ async function diffCommand(id, options = {}) {
2722
+ let response;
2723
+ try {
2724
+ response = await get(`/api/deploy/changes/${id}`);
2725
+ } catch (err) {
2726
+ console.log(pc.red(`Failed to fetch changes: ${err instanceof Error ? err.message : err}`));
2727
+ process.exit(1);
2728
+ }
2729
+ let { changes } = response;
2730
+ if (options.breaking) changes = changes.filter((c) => c.breaking);
2731
+ if (options.format === "json") {
2732
+ console.log(JSON.stringify(changes, null, 2));
2733
+ return;
2734
+ }
2735
+ console.log();
2736
+ console.log(pc.bold(`Changes in deployment ${id.slice(0, 8)}`));
2737
+ console.log();
2738
+ if (changes.length === 0) {
2739
+ console.log(pc.dim(" No changes recorded."));
2740
+ console.log();
2741
+ return;
2742
+ }
2743
+ for (const c of changes) {
2744
+ const prefix = c.changeType === "added" ? pc.green("+") : c.changeType === "removed" ? pc.red("-") : pc.yellow("~");
2745
+ const label = `${capitalize(c.changeType)} ${c.objectType}: ${c.objectName}`;
2746
+ const breakingTag = c.breaking ? pc.red(" BREAKING") : "";
2747
+ console.log(` ${prefix} ${label}${breakingTag}`);
2748
+ const details = [];
2749
+ if (c.summary) details.push(c.summary);
2750
+ if (details.length > 0) console.log(pc.dim(` ${details.join(" | ")}`));
2751
+ if (c.changeType === "modified" && c.previousDefinition && c.newDefinition) for (const key of Object.keys(c.newDefinition)) {
2752
+ const oldVal = c.previousDefinition[key];
2753
+ const newVal = c.newDefinition[key];
2754
+ if (oldVal !== newVal && oldVal !== void 0 && newVal !== void 0) console.log(pc.dim(` ${key}: ${JSON.stringify(oldVal)} -> ${JSON.stringify(newVal)}`));
2755
+ }
2756
+ if (c.annotation) console.log(` ${pc.cyan("\"" + c.annotation + "\"")}`);
2757
+ console.log();
2758
+ }
2759
+ const added = changes.filter((c) => c.changeType === "added").length;
2760
+ const modified = changes.filter((c) => c.changeType === "modified").length;
2761
+ const removed = changes.filter((c) => c.changeType === "removed").length;
2762
+ const breaking = changes.filter((c) => c.breaking).length;
2763
+ const parts = [
2764
+ `${added} added`,
2765
+ `${modified} modified`,
2766
+ `${removed} removed`
2767
+ ];
2768
+ if (breaking > 0) parts.push(pc.red(`${breaking} breaking`));
2769
+ console.log(pc.dim(`Summary: ${parts.join(", ")}`));
2770
+ console.log();
2771
+ }
2772
+ function capitalize(s) {
2773
+ return s.charAt(0).toUpperCase() + s.slice(1);
2774
+ }
2775
+
2535
2776
  //#endregion
2536
2777
  //#region src/commands/mcp.ts
2537
2778
  const MCP_URL = "https://mcp.bonnard.dev/mcp";
@@ -2857,7 +3098,10 @@ datasource.command("remove").description("Remove a data source from .bon/datasou
2857
3098
  datasource.command("push").description("Push a local data source to Bonnard server (requires login)").argument("<name>", "Data source name from .bon/datasources.yaml").option("--force", "Overwrite if already exists on remote").action(datasourcePushCommand);
2858
3099
  program.command("preview").description("Preview data from a local warehouse using raw SQL (for development/exploration)").argument("<datasource>", "Data source name from .bon/datasources.yaml").argument("<sql>", "SQL query to execute").option("--schema <schema>", "Override schema").option("--database <database>", "Override database").option("--limit <limit>", "Max rows to return", "1000").option("--format <format>", "Output format: toon or json", "toon").action(previewCommand);
2859
3100
  program.command("validate").description("Validate YAML syntax in bonnard/cubes/ and bonnard/views/").option("--test-connection", "Also test datasource connections (warns on failure, doesn't block)").action(validateCommand);
2860
- program.command("deploy").description("Deploy cubes and views to Bonnard. Requires login, validates, tests connections (fails on error)").option("--ci", "Non-interactive mode (fail if missing datasources)").option("--push-datasources", "Auto-push missing datasources without prompting").action(deployCommand);
3101
+ program.command("deploy").description("Deploy cubes and views to Bonnard. Requires login, validates, tests connections (fails on error)").option("--ci", "Non-interactive mode (fail if missing datasources)").option("--push-datasources", "Auto-push missing datasources without prompting").requiredOption("-m, --message <text>", "Deploy message describing your changes").action(deployCommand);
3102
+ program.command("deployments").description("List deployment history").option("--all", "Show all deployments (default: last 10)").option("--format <format>", "Output format: table or json", "table").action(deploymentsCommand);
3103
+ program.command("diff").description("Show changes in a deployment").argument("<id>", "Deployment ID").option("--format <format>", "Output format: table or json", "table").option("--breaking", "Show only breaking changes").action(diffCommand);
3104
+ program.command("annotate").description("Annotate deployment changes with reasoning").argument("<id>", "Deployment ID").option("--data <json>", "Annotations JSON").action(annotateCommand);
2861
3105
  program.command("mcp").description("MCP connection info and setup instructions").action(mcpCommand).command("test").description("Test MCP server connectivity").action(mcpTestCommand);
2862
3106
  program.command("query").description("Execute a query against the deployed semantic layer").argument("<query>", "JSON query or SQL (with --sql flag)").option("--sql", "Use SQL API instead of JSON format").option("--limit <limit>", "Max rows to return").option("--format <format>", "Output format: toon or json", "toon").action(cubeQueryCommand);
2863
3107
  program.command("docs").description("Browse documentation for building cubes and views").argument("[topic]", "Topic to display (e.g., cubes, cubes.measures)").option("-r, --recursive", "Show topic and all child topics").option("-s, --search <query>", "Search topics for a keyword").option("-f, --format <format>", "Output format: markdown or json", "markdown").action(docsCommand).command("schema").description("Show JSON schema for a type (cube, view, measure, etc.)").argument("<type>", "Schema type to display").action(docsSchemaCommand);
@@ -59,6 +59,15 @@ function checkMissingDescriptions(files) {
59
59
  } catch {}
60
60
  return missing;
61
61
  }
62
+ function checkMissingDataSource(files) {
63
+ const missing = [];
64
+ for (const file of files) try {
65
+ const parsed = YAML.parse(file.content);
66
+ if (!parsed) continue;
67
+ for (const cube of parsed.cubes || []) if (cube.name && !cube.data_source) missing.push(cube.name);
68
+ } catch {}
69
+ return missing;
70
+ }
62
71
  function createModelRepository(projectPath) {
63
72
  const paths = getProjectPaths(projectPath);
64
73
  const cubesDir = paths.cubes;
@@ -79,7 +88,8 @@ async function validate(projectPath) {
79
88
  errors: [],
80
89
  cubes: [],
81
90
  views: [],
82
- missingDescriptions: []
91
+ missingDescriptions: [],
92
+ cubesMissingDataSource: []
83
93
  };
84
94
  try {
85
95
  const { cubeEvaluator } = await compile(repo, {});
@@ -92,7 +102,8 @@ async function validate(projectPath) {
92
102
  errors: [],
93
103
  cubes,
94
104
  views,
95
- missingDescriptions: checkMissingDescriptions(files)
105
+ missingDescriptions: checkMissingDescriptions(files),
106
+ cubesMissingDataSource: checkMissingDataSource(files)
96
107
  };
97
108
  } catch (err) {
98
109
  const raw = err.messages ?? err.message ?? String(err);
@@ -101,7 +112,8 @@ async function validate(projectPath) {
101
112
  errors: Array.isArray(raw) ? raw : [raw],
102
113
  cubes: [],
103
114
  views: [],
104
- missingDescriptions: []
115
+ missingDescriptions: [],
116
+ cubesMissingDataSource: []
105
117
  };
106
118
  }
107
119
  }
@@ -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
 
@@ -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