@revos/cli 0.3.2 → 0.3.5
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.
- package/README.md +2 -2
- package/dist/adapters/oclif/commands/action-runs/get.mjs +1 -1
- package/dist/adapters/oclif/commands/action-runs/list.mjs +1 -1
- package/dist/adapters/oclif/commands/actions/get-input-schema.mjs +2 -2
- package/dist/adapters/oclif/commands/actions/get-params-schema.mjs +2 -2
- package/dist/adapters/oclif/commands/actions/get.mjs +1 -1
- package/dist/adapters/oclif/commands/actions/list.mjs +2 -2
- package/dist/adapters/oclif/commands/ai-instructions/create.mjs +1 -1
- package/dist/adapters/oclif/commands/ai-instructions/delete.mjs +1 -1
- package/dist/adapters/oclif/commands/ai-instructions/get.mjs +1 -1
- package/dist/adapters/oclif/commands/ai-instructions/list.mjs +1 -1
- package/dist/adapters/oclif/commands/ai-instructions/update.mjs +1 -1
- package/dist/adapters/oclif/commands/api.mjs +2 -2
- package/dist/adapters/oclif/commands/apply.mjs +2 -2
- package/dist/adapters/oclif/commands/auth/login.mjs +2 -2
- package/dist/adapters/oclif/commands/auth/logout.mjs +2 -2
- package/dist/adapters/oclif/commands/auth/status.mjs +2 -2
- package/dist/adapters/oclif/commands/connections/create.mjs +1 -1
- package/dist/adapters/oclif/commands/connections/delete.mjs +1 -1
- package/dist/adapters/oclif/commands/connections/get.mjs +1 -1
- package/dist/adapters/oclif/commands/connections/list.mjs +1 -1
- package/dist/adapters/oclif/commands/connections/update.mjs +1 -1
- package/dist/adapters/oclif/commands/cubes/create.mjs +1 -1
- package/dist/adapters/oclif/commands/cubes/delete.mjs +1 -1
- package/dist/adapters/oclif/commands/cubes/get.mjs +1 -1
- package/dist/adapters/oclif/commands/cubes/list.mjs +1 -1
- package/dist/adapters/oclif/commands/cubes/meta.mjs +2 -2
- package/dist/adapters/oclif/commands/cubes/query.mjs +2 -2
- package/dist/adapters/oclif/commands/cubes/update.mjs +1 -1
- package/dist/adapters/oclif/commands/diff.mjs +2 -2
- package/dist/adapters/oclif/commands/gservice-account-keys/get.mjs +1 -1
- package/dist/adapters/oclif/commands/gservice-account-keys/reveal.mjs +2 -2
- package/dist/adapters/oclif/commands/gservice-accounts/create.mjs +1 -1
- package/dist/adapters/oclif/commands/gservice-accounts/delete.mjs +1 -1
- package/dist/adapters/oclif/commands/gservice-accounts/get.mjs +1 -1
- package/dist/adapters/oclif/commands/gservice-accounts/list.mjs +1 -1
- package/dist/adapters/oclif/commands/init.mjs +2 -2
- package/dist/adapters/oclif/commands/org/create.mjs +1 -1
- package/dist/adapters/oclif/commands/org/current.mjs +2 -2
- package/dist/adapters/oclif/commands/org/get.mjs +1 -1
- package/dist/adapters/oclif/commands/org/list.mjs +2 -2
- package/dist/adapters/oclif/commands/org/switch.mjs +2 -2
- package/dist/adapters/oclif/commands/pull.mjs +2 -2
- package/dist/adapters/oclif/commands/score-groups/create.mjs +1 -1
- package/dist/adapters/oclif/commands/score-groups/delete.mjs +1 -1
- package/dist/adapters/oclif/commands/score-groups/get.mjs +1 -1
- package/dist/adapters/oclif/commands/score-groups/list.mjs +1 -1
- package/dist/adapters/oclif/commands/score-groups/update.mjs +1 -1
- package/dist/adapters/oclif/commands/scores/create.mjs +1 -1
- package/dist/adapters/oclif/commands/scores/delete.mjs +1 -1
- package/dist/adapters/oclif/commands/scores/list.mjs +1 -1
- package/dist/adapters/oclif/commands/scores/update.mjs +1 -1
- package/dist/adapters/oclif/commands/segments/create.mjs +1 -1
- package/dist/adapters/oclif/commands/segments/delete.mjs +1 -1
- package/dist/adapters/oclif/commands/segments/evaluate.mjs +2 -2
- package/dist/adapters/oclif/commands/segments/get-evaluation-history.mjs +2 -2
- package/dist/adapters/oclif/commands/segments/get-version.mjs +2 -2
- package/dist/adapters/oclif/commands/segments/get.mjs +1 -1
- package/dist/adapters/oclif/commands/segments/list-versions.mjs +2 -2
- package/dist/adapters/oclif/commands/segments/list.mjs +1 -1
- package/dist/adapters/oclif/commands/segments/restore-version.mjs +2 -2
- package/dist/adapters/oclif/commands/segments/update.mjs +1 -1
- package/dist/adapters/oclif/commands/sources/create.mjs +2 -2
- package/dist/adapters/oclif/commands/sources/delete.mjs +1 -1
- package/dist/adapters/oclif/commands/sources/get.mjs +1 -1
- package/dist/adapters/oclif/commands/sources/list-streams.mjs +2 -2
- package/dist/adapters/oclif/commands/sources/list.mjs +1 -1
- package/dist/adapters/oclif/commands/sources/update.mjs +2 -2
- package/dist/adapters/oclif/commands/status.mjs +3 -3
- package/dist/adapters/oclif/commands/table-views/create.mjs +1 -1
- package/dist/adapters/oclif/commands/table-views/delete.mjs +1 -1
- package/dist/adapters/oclif/commands/table-views/list.mjs +1 -1
- package/dist/adapters/oclif/commands/table-views/update.mjs +1 -1
- package/dist/adapters/oclif/commands/tables/create.mjs +1 -1
- package/dist/adapters/oclif/commands/tables/delete.mjs +1 -1
- package/dist/adapters/oclif/commands/tables/get.mjs +1 -1
- package/dist/adapters/oclif/commands/tables/list.mjs +1 -1
- package/dist/adapters/oclif/commands/tables/update.mjs +1 -1
- package/dist/{base.command-CnVb4RG6.mjs → base.command-CWGrSq73.mjs} +1 -1
- package/dist/{core-CY9pC37x.mjs → core-CChCxudp.mjs} +3 -0
- package/dist/{factory-DTqayaCF.mjs → factory-SazXfu26.mjs} +2 -2
- package/dist/index.mjs +1 -1
- package/dist/{presets-mJzFGMhG.mjs → presets-Blw7i6BW.mjs} +2 -2
- package/dist/templates/README.md +1 -0
- package/dist/templates/skills/create-connections/SKILL.md +4 -2
- package/dist/templates/skills/create-cubes/SKILL.md +3 -0
- package/dist/templates/skills/create-cubes/references/cube-examples.md +13 -0
- package/dist/templates/skills/query-semantic-model/SKILL.md +347 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { G as ApiError, I as setAuthConfig, U as loadCredentials, i as formatEnvMismatchError, l as resolveProjectContext, o as formatProjectOrgFlagError, r as formatCredentialsMismatchWarning, u as validateEnvAgainstProject } from "./core-
|
|
1
|
+
import { G as ApiError, I as setAuthConfig, U as loadCredentials, i as formatEnvMismatchError, l as resolveProjectContext, o as formatProjectOrgFlagError, r as formatCredentialsMismatchWarning, u as validateEnvAgainstProject } from "./core-CChCxudp.mjs";
|
|
2
2
|
import { Command, Flags } from "@oclif/core";
|
|
3
3
|
import { makeTable } from "@oclif/table";
|
|
4
4
|
//#region src/adapters/oclif/base.command.ts
|
|
@@ -2254,6 +2254,7 @@ var InitService = class InitService {
|
|
|
2254
2254
|
".claude/skills/create-dbt-transformations",
|
|
2255
2255
|
".claude/skills/create-dbt-transformations/references",
|
|
2256
2256
|
".claude/skills/load-sample-data",
|
|
2257
|
+
".claude/skills/query-semantic-model",
|
|
2257
2258
|
".claude/skills/visualize-semantic-model",
|
|
2258
2259
|
".claude/skills/visualize-semantic-model/scripts",
|
|
2259
2260
|
"dbt/models/bronze",
|
|
@@ -2292,6 +2293,7 @@ var InitService = class InitService {
|
|
|
2292
2293
|
".claude/skills/create-dbt-transformations/references/schema-conventions.md",
|
|
2293
2294
|
".claude/skills/create-dbt-transformations/references/edge-cases.md",
|
|
2294
2295
|
".claude/skills/load-sample-data/SKILL.md",
|
|
2296
|
+
".claude/skills/query-semantic-model/SKILL.md",
|
|
2295
2297
|
".claude/skills/visualize-semantic-model/SKILL.md",
|
|
2296
2298
|
".claude/skills/visualize-semantic-model/scripts/render_graph.py",
|
|
2297
2299
|
"dbt/models/bronze/.gitkeep",
|
|
@@ -2474,6 +2476,7 @@ var InitService = class InitService {
|
|
|
2474
2476
|
".claude/skills/create-dbt-transformations/references/schema-conventions.md": this.renderTemplate("skills/create-dbt-transformations/references/schema-conventions.md", {}),
|
|
2475
2477
|
".claude/skills/create-dbt-transformations/references/edge-cases.md": this.renderTemplate("skills/create-dbt-transformations/references/edge-cases.md", {}),
|
|
2476
2478
|
".claude/skills/load-sample-data/SKILL.md": this.renderTemplate("skills/load-sample-data/SKILL.md", {}),
|
|
2479
|
+
".claude/skills/query-semantic-model/SKILL.md": this.renderTemplate("skills/query-semantic-model/SKILL.md", {}),
|
|
2477
2480
|
".claude/skills/visualize-semantic-model/SKILL.md": this.renderTemplate("skills/visualize-semantic-model/SKILL.md", {}),
|
|
2478
2481
|
".claude/skills/visualize-semantic-model/scripts/render_graph.py": this.renderTemplate("skills/visualize-semantic-model/scripts/render_graph.py", {}),
|
|
2479
2482
|
"dbt/models/bronze/.gitkeep": "",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { E as getConfig, b as createApiClient } from "./core-
|
|
2
|
-
import { t as BaseCommand } from "./base.command-
|
|
1
|
+
import { E as getConfig, b as createApiClient } from "./core-CChCxudp.mjs";
|
|
2
|
+
import { t as BaseCommand } from "./base.command-CWGrSq73.mjs";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import chalk from "chalk";
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as buildAuthorizationUrl, B as deleteCredentials, C as formatError, D as resolveApiUrl, E as getConfig, F as refreshAccessToken, G as ApiError, H as isTokenExpired, I as setAuthConfig, L as setAuthEnv, M as generatePKCEChallenge, N as getActiveAuthConfig, O as performOAuthLogin, P as getUserInfo, R as tokenResponseToCredentials, S as resolveAppUrl, T as DEFAULT_API_URL, U as loadCredentials, V as getCredentialsPath, W as saveCredentials, a as formatInProjectSwitchWarning, b as createApiClient, c as isInsideProject, d as iac_exports, i as formatEnvMismatchError, j as exchangeCodeForTokens, k as AUTH_ENVS, l as resolveProjectContext, o as formatProjectOrgFlagError, r as formatCredentialsMismatchWarning, s as renderProjectContextLine, t as InitService, u as validateEnvAgainstProject, w as sanitizeFileName, x as unwrap, z as startOAuthServer } from "./core-
|
|
1
|
+
import { A as buildAuthorizationUrl, B as deleteCredentials, C as formatError, D as resolveApiUrl, E as getConfig, F as refreshAccessToken, G as ApiError, H as isTokenExpired, I as setAuthConfig, L as setAuthEnv, M as generatePKCEChallenge, N as getActiveAuthConfig, O as performOAuthLogin, P as getUserInfo, R as tokenResponseToCredentials, S as resolveAppUrl, T as DEFAULT_API_URL, U as loadCredentials, V as getCredentialsPath, W as saveCredentials, a as formatInProjectSwitchWarning, b as createApiClient, c as isInsideProject, d as iac_exports, i as formatEnvMismatchError, j as exchangeCodeForTokens, k as AUTH_ENVS, l as resolveProjectContext, o as formatProjectOrgFlagError, r as formatCredentialsMismatchWarning, s as renderProjectContextLine, t as InitService, u as validateEnvAgainstProject, w as sanitizeFileName, x as unwrap, z as startOAuthServer } from "./core-CChCxudp.mjs";
|
|
2
2
|
export { AUTH_ENVS, ApiError, DEFAULT_API_URL, InitService, buildAuthorizationUrl, createApiClient, deleteCredentials, exchangeCodeForTokens, formatCredentialsMismatchWarning, formatEnvMismatchError, formatError, formatInProjectSwitchWarning, formatProjectOrgFlagError, generatePKCEChallenge, getActiveAuthConfig, getConfig, getCredentialsPath, getUserInfo, iac_exports as iac, isInsideProject, isTokenExpired, loadCredentials, performOAuthLogin, refreshAccessToken, renderProjectContextLine, resolveApiUrl, resolveAppUrl, resolveProjectContext, sanitizeFileName, saveCredentials, setAuthConfig, setAuthEnv, startOAuthServer, tokenResponseToCredentials, unwrap, validateEnvAgainstProject };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { x as unwrap } from "./core-
|
|
2
|
-
import { n as createListRender, r as defineApiCommand, t as bodyFlag } from "./factory-
|
|
1
|
+
import { x as unwrap } from "./core-CChCxudp.mjs";
|
|
2
|
+
import { n as createListRender, r as defineApiCommand, t as bodyFlag } from "./factory-SazXfu26.mjs";
|
|
3
3
|
import { Args, Flags } from "@oclif/core";
|
|
4
4
|
//#region src/adapters/oclif/presets.ts
|
|
5
5
|
function camelToKebab(s) {
|
package/dist/templates/README.md
CHANGED
|
@@ -69,6 +69,7 @@ This project ships pre-installed Claude skills under `.claude/skills/` for the c
|
|
|
69
69
|
- **`create-cubes`** — build Cube.dev semantic models from gold-layer tables.
|
|
70
70
|
- **`create-dbt-transformations`** — write bronze→silver→gold dbt models.
|
|
71
71
|
- **`load-sample-data`** — seed BigQuery with example data when the warehouse is empty.
|
|
72
|
+
- **`query-semantic-model`** — run a Cube.js query and render the result inline in chat as a table + ASCII bar chart.
|
|
72
73
|
- **`visualize-semantic-model`** — render the cube graph as an image.
|
|
73
74
|
|
|
74
75
|
Inside the container, ask Claude things like _"create a Connection for our Stripe source"_ or _"build a silver model for the orders table"_ and it will use the matching skill.
|
|
@@ -21,7 +21,7 @@ The hard part is **stream selection** and **sync-mode choice**, not the YAML sha
|
|
|
21
21
|
|
|
22
22
|
## Prerequisites
|
|
23
23
|
|
|
24
|
-
- The Source already exists on the server (you'll get its `id` via `revos sources list`). If the user wants to ingest from a source that isn't created yet, stop and tell them to run `revos sources create` first — source configuration (connector picker, credentials, OAuth) lives in the RevOS UI.
|
|
24
|
+
- The Source already exists on the server (you'll get its `id` via `revos sources list`). If the user wants to ingest from a source that isn't created yet, stop and tell them to run `revos sources create` first — source configuration (connector picker, credentials, OAuth) lives in the RevOS UI. Once the user confirms they've returned from the UI, **re-run `revos connections list --json` and `revos sources list --json` before continuing** — the UI may have auto-created a connection for the new source. If a matching connection already exists, offer to pull it with `revos pull` rather than authoring a duplicate.
|
|
25
25
|
- `revos auth status` shows authenticated. If not, ask the user to run `revos auth login`.
|
|
26
26
|
|
|
27
27
|
---
|
|
@@ -44,7 +44,9 @@ After deriving sync mode + cursor + primary key for each selected stream, presen
|
|
|
44
44
|
|
|
45
45
|
## Phase 1: Identify the Source
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
First, run `revos connections list --json` to see what connections already exist. If the user just returned from the web UI, this re-fetch is essential — the UI may have created a connection automatically. If you find a connection that matches the user's stated goal, surface it before proceeding so you don't create a duplicate.
|
|
48
|
+
|
|
49
|
+
Then run `revos sources list --json` and match on the server-side `name` (or run `revos sources list` for a scannable table). Record the source's `id` — that goes into the Connection YAML as `spec.source.id`.
|
|
48
50
|
|
|
49
51
|
If no source was named, ask the user which one. Don't proceed without an id.
|
|
50
52
|
|
|
@@ -247,6 +247,9 @@ Key rules:
|
|
|
247
247
|
6. Every cube **must** include a SQL-based `refresh_key`. Use `SELECT MAX(<timestamp_col>)` with columns in this priority: `_airbyte_extracted_at` (present on all Airbyte sources), `updated_at`/`modified_at` (CDC streams), `created_at` (insert-only facts). Only use `every: <interval>` as absolute last resort when **no timestamp column exists in the table** — add a YAML comment explaining why (e.g. `# no timestamp column available`).
|
|
248
248
|
7. `refresh_key.sql` references the same table as `sql_table`.
|
|
249
249
|
8. Tag unvalidated joins with `# UNVALIDATED: <reason>`.
|
|
250
|
+
9. For cubes derived from data ingested by a Connection (i.e. the gold model traces back to bronze tables produced by `revos apply` on a `Connection`), set `meta.abConnectionId: <connection-id>`. This groups cubes by their originating connection in the UI. Resolve the connection id with `revos connections list --json` — match by `spec.prefix` against the bronze table prefix the gold model reads from. Bridge / junction cubes built on top of connection-sourced models inherit the same `abConnectionId`. Cubes derived from purely local data (e.g. hand-written silver/gold models with no upstream connection) omit `abConnectionId`.
|
|
251
|
+
10. For cubes whose rows have a natural human-readable label (companies, contacts, deals, tickets, users, projects, …), set `meta.nameDimension: <short-dimension-name>`. The value is the **short** dimension key (no `${CUBE}.` prefix and no cube-name prefix) — e.g. `properties_name`, `displayName`, `dealname`. The frontend uses this to pick the column shown as the entity's name when the cube is added as a table (see `useCubeFromMeta` in the frontend). Pick the dimension that a user would recognize as "the name of this thing"; if no such single column exists (pure join / bridge cubes, fact tables, event logs), omit `nameDimension`. The dimension must exist on this cube under `dimensions:`.
|
|
252
|
+
11. `meta` is closed: only `abConnectionId` and `nameDimension` are allowed today. Omit the whole `meta` block if neither applies.
|
|
250
253
|
|
|
251
254
|
See [references/cube-examples.md](references/cube-examples.md) for canonical standard cube, bridge cube, join direction examples, and refresh key variants.
|
|
252
255
|
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
name: hubspot_companies
|
|
20
20
|
sql_table: "`revos_1737556292084.gold_hubspot_companies`"
|
|
21
21
|
|
|
22
|
+
meta:
|
|
23
|
+
abConnectionId: conn_01HZX7K9P6QABCD
|
|
24
|
+
nameDimension: properties_name
|
|
25
|
+
|
|
22
26
|
joins:
|
|
23
27
|
companies_deals:
|
|
24
28
|
sql: "${CUBE}.id = ${companies_deals}.company_id"
|
|
@@ -38,6 +42,10 @@ dimensions:
|
|
|
38
42
|
type: string
|
|
39
43
|
primary_key: true
|
|
40
44
|
|
|
45
|
+
properties_name:
|
|
46
|
+
sql: "${CUBE}.properties_name"
|
|
47
|
+
type: string
|
|
48
|
+
|
|
41
49
|
airbyte_extracted_at:
|
|
42
50
|
sql: "${CUBE}._airbyte_extracted_at"
|
|
43
51
|
type: time
|
|
@@ -53,6 +61,8 @@ Notes:
|
|
|
53
61
|
3. The join references `${companies_deals}` — the cube name of a bridge cube defined in `cubes/companies_deals.yml`.
|
|
54
62
|
4. Only `_airbyte_extracted_at` is exposed from Airbyte metadata, as `airbyte_extracted_at`.
|
|
55
63
|
5. `refresh_key.sql` uses the same fully qualified table name as `sql_table`.
|
|
64
|
+
6. `meta.abConnectionId` ties this cube back to the Connection that ingests its raw data — get the id from `revos connections list --json`. Omit it for cubes built on purely local models with no upstream connection.
|
|
65
|
+
7. `meta.nameDimension` is the **short** name of the dimension that represents this entity's human-readable label — here `properties_name`, referenced as `${CUBE}.properties_name`. The dimension must exist under `dimensions:`. The frontend reads `meta.nameDimension` to pick the "name" column when this cube is added as a table. Omit `nameDimension` for cubes that don't have a single natural label (bridges, fact tables, event logs). `meta` currently allows only `abConnectionId` and `nameDimension`; omit the whole `meta` block if neither applies.
|
|
56
66
|
|
|
57
67
|
---
|
|
58
68
|
|
|
@@ -63,6 +73,9 @@ name: companies_deals
|
|
|
63
73
|
sql_table: "`revos_1737556292084.gold_companies_deals`"
|
|
64
74
|
public: false
|
|
65
75
|
|
|
76
|
+
meta:
|
|
77
|
+
abConnectionId: conn_01HZX7K9P6QABCD
|
|
78
|
+
|
|
66
79
|
joins:
|
|
67
80
|
hubspot_companies:
|
|
68
81
|
relationship: many_to_one
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: query-semantic-model
|
|
3
|
+
description: >
|
|
4
|
+
Run a Cube.js query against the semantic model and show the result inline in chat as
|
|
5
|
+
an ASCII table and (where it makes sense) an ASCII bar chart. Use whenever the user
|
|
6
|
+
asks a business question that can be answered from the cubes — "how many orders…",
|
|
7
|
+
"top N…", "revenue by…", "trend over time", "compare X to Y" — or explicitly asks to
|
|
8
|
+
"query the semantic model", "run a cube query", "show me a chart", "plot…", "graph…".
|
|
9
|
+
Discovers measures and dimensions via `revos cubes meta`, executes the query with
|
|
10
|
+
`revos cubes query`, and renders the results without leaving the chat.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Query Semantic Model
|
|
14
|
+
|
|
15
|
+
Answer business questions by querying the org's semantic model and rendering the result
|
|
16
|
+
directly in the chat — no notebook, no UI hop. The CLI already exposes everything you
|
|
17
|
+
need: `revos cubes meta` lists the available cubes / measures / dimensions, and
|
|
18
|
+
`revos cubes query` runs a Cube.js query and returns the rows.
|
|
19
|
+
|
|
20
|
+
This skill is the "last mile" after Bronze → Silver → Gold → Cubes have been built and
|
|
21
|
+
applied. It turns the semantic model into something you can actually look at.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Step 1: Discover what's queryable
|
|
26
|
+
|
|
27
|
+
Before guessing member names, list what the org's semantic model actually exposes:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
revos cubes meta --json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The response carries an array of cubes, each with `measures`, `dimensions`, `segments`,
|
|
34
|
+
and a `name`. Cube members are addressed as `<CubeName>.<member>` — for example
|
|
35
|
+
`gold_order_items_enriched.count` or `gold_order_items_enriched.order_date`.
|
|
36
|
+
|
|
37
|
+
Pick the cube and members that match the user's question. If the question can't be
|
|
38
|
+
answered from the available members, say so — don't invent member names.
|
|
39
|
+
|
|
40
|
+
### Map the join graph from the local cube files
|
|
41
|
+
|
|
42
|
+
`revos cubes meta` tells you which members exist; the local `cubes/` folder tells you
|
|
43
|
+
**how cubes are connected**. Read every `*.yml` file in `cubes/` and pull out each
|
|
44
|
+
cube's `joins:` block — that's how the user-defined part of the semantic model is
|
|
45
|
+
wired up. Example:
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
# cubes/gold_order_items_enriched.yml
|
|
49
|
+
spec:
|
|
50
|
+
definition:
|
|
51
|
+
name: gold_order_items_enriched
|
|
52
|
+
joins:
|
|
53
|
+
gold_users_with_order_stats:
|
|
54
|
+
relationship: many_to_one
|
|
55
|
+
sql: "${CUBE}.user_id = ${gold_users_with_order_stats}.user_id"
|
|
56
|
+
gold_product_performance:
|
|
57
|
+
relationship: many_to_one
|
|
58
|
+
sql: "${CUBE}.product_id = ${gold_product_performance}.product_id"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
From this you can compute, for any pair of cubes, how many distinct join paths exist
|
|
62
|
+
between them. That's what lets you pick the right path in Step 2 instead of letting
|
|
63
|
+
Cube.js guess one for you.
|
|
64
|
+
|
|
65
|
+
**Caveat — system cubes are not in `cubes/`.** RevOS generates a handful of cubes
|
|
66
|
+
server-side (scoring, segments, model overlays, pre-aggregation overlays). Those
|
|
67
|
+
never appear as YAML in the project. The full member list is only visible through
|
|
68
|
+
`revos cubes meta`. So:
|
|
69
|
+
|
|
70
|
+
- Use the `cubes/` folder to **see the user-defined join graph**.
|
|
71
|
+
- Use `revos cubes meta` to **see every member that can actually be queried**,
|
|
72
|
+
including system cubes.
|
|
73
|
+
|
|
74
|
+
When the user's question touches a system cube (e.g. "show me users by segment",
|
|
75
|
+
"top scoring entities"), the join graph from `cubes/` will be incomplete. Don't
|
|
76
|
+
assume there's no path — list the meta and look for it there.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Step 2: Build the query — and pin the join path when it matters
|
|
81
|
+
|
|
82
|
+
A Cube.js query is a JSON object. The shapes you'll use most often:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{ "measures": ["gold_order_items_enriched.count"] }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"measures": ["gold_order_items_enriched.count"],
|
|
91
|
+
"dimensions": ["gold_order_items_enriched.traffic_source"],
|
|
92
|
+
"order": { "gold_order_items_enriched.count": "desc" },
|
|
93
|
+
"limit": 10
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Time series (the chart-friendliest shape):
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"measures": ["gold_order_items_enriched.revenue"],
|
|
102
|
+
"timeDimensions": [
|
|
103
|
+
{
|
|
104
|
+
"dimension": "gold_order_items_enriched.order_date",
|
|
105
|
+
"granularity": "month",
|
|
106
|
+
"dateRange": "last 12 months"
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Rules of thumb:
|
|
113
|
+
|
|
114
|
+
- One measure + one dimension → bar chart by category.
|
|
115
|
+
- One measure + one `timeDimensions` with `granularity` → bar chart over time.
|
|
116
|
+
- Multiple measures or multiple dimensions → render as an ASCII table only; don't try
|
|
117
|
+
to draw a chart.
|
|
118
|
+
- Always cap with `limit` (default 20) so a stray query can't return a million rows.
|
|
119
|
+
|
|
120
|
+
### Join paths — and how to pin them with a hint
|
|
121
|
+
|
|
122
|
+
Cube.js infers the join path from the cubes that appear in `measures` /
|
|
123
|
+
`dimensions` / `filters`. **When more than one path connects them, Cube.js picks
|
|
124
|
+
one for you — and the answer changes depending on which.** This is the single
|
|
125
|
+
biggest source of "the number looks wrong" bugs in a semantic-model query.
|
|
126
|
+
|
|
127
|
+
You spell the path out by prefixing the member with the cubes you want Cube.js to
|
|
128
|
+
walk through, dot-separated. The last segment is the actual member; everything
|
|
129
|
+
before it is the join hint. Examples:
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
# no hint — Cube.js picks a path
|
|
133
|
+
gold_users_with_order_stats.lifetime_revenue
|
|
134
|
+
|
|
135
|
+
# hint: reach lifetime_revenue via the fact spine, then the users mart
|
|
136
|
+
gold_order_items_enriched.gold_users_with_order_stats.lifetime_revenue
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The hint syntax works the same on measures, dimensions, and filter members.
|
|
140
|
+
|
|
141
|
+
**When you need a hint (red flags):**
|
|
142
|
+
|
|
143
|
+
1. Two cubes in the query are connected by **more than one path** in `cubes/`.
|
|
144
|
+
Common shape: a fact spine joins to two dimension cubes, and one of the dimension
|
|
145
|
+
cubes _also_ joins to the other (e.g. orders → users, orders → products,
|
|
146
|
+
products → users via `last_purchaser_id`). Without a hint, Cube.js may pick the
|
|
147
|
+
non-obvious path and quietly double- or under-count.
|
|
148
|
+
2. The user's question implicitly names the path ("revenue **per order item** by
|
|
149
|
+
user country" vs "revenue **per user** by user country") — different grains,
|
|
150
|
+
different paths, different numbers.
|
|
151
|
+
3. Counts come back smaller than the smallest cube would allow, or larger than the
|
|
152
|
+
largest fact would allow. That's a sign Cube.js fanned out through the wrong
|
|
153
|
+
join.
|
|
154
|
+
4. The cube has a many-to-many relationship in the chain. Always pin it.
|
|
155
|
+
5. You added a measure and the number changed dramatically (more than ~10%) without
|
|
156
|
+
the dimension set changing — Cube.js probably re-routed the join.
|
|
157
|
+
|
|
158
|
+
**Worked example — same question, two answers.**
|
|
159
|
+
|
|
160
|
+
Cubes (from `cubes/`):
|
|
161
|
+
|
|
162
|
+
```text
|
|
163
|
+
gold_order_items_enriched ──many_to_one──▶ gold_users_with_order_stats
|
|
164
|
+
gold_order_items_enriched ──many_to_one──▶ gold_product_performance
|
|
165
|
+
gold_product_performance ──many_to_one──▶ gold_users_with_order_stats (last_purchaser_id)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Question: "Total revenue by user country."
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
// Path A — order items → users directly (CORRECT for per-item revenue)
|
|
172
|
+
{
|
|
173
|
+
"measures": ["gold_order_items_enriched.revenue"],
|
|
174
|
+
"dimensions": [
|
|
175
|
+
"gold_order_items_enriched.gold_users_with_order_stats.country"
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
// Path B — order items → product → last_purchaser (WRONG: revenue gets
|
|
182
|
+
// attributed to the last buyer's country, not the actual buyer's)
|
|
183
|
+
{
|
|
184
|
+
"measures": ["gold_order_items_enriched.revenue"],
|
|
185
|
+
"dimensions": [
|
|
186
|
+
"gold_order_items_enriched.gold_product_performance.gold_users_with_order_stats.country"
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The two queries return wildly different numbers. Always state which path you used
|
|
192
|
+
and **why** in the explanation underneath the chart so the user can spot a wrong
|
|
193
|
+
choice.
|
|
194
|
+
|
|
195
|
+
**Worked example — filter via a specific path.**
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"measures": ["gold_order_items_enriched.revenue"],
|
|
200
|
+
"dimensions": ["gold_order_items_enriched.traffic_source"],
|
|
201
|
+
"filters": [
|
|
202
|
+
{
|
|
203
|
+
"member": "gold_order_items_enriched.gold_users_with_order_stats.country",
|
|
204
|
+
"operator": "equals",
|
|
205
|
+
"values": ["DE"]
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Rules for choosing a path:**
|
|
212
|
+
|
|
213
|
+
- Default to the **shortest** path that goes through the **fact spine** the
|
|
214
|
+
question is naturally grained on (revenue per order → spine =
|
|
215
|
+
`gold_order_items_enriched`; users with X → spine = `gold_users_with_order_stats`).
|
|
216
|
+
- Never silently take the alternate path. If two paths are plausible, ask the user
|
|
217
|
+
which grain they meant before running the query.
|
|
218
|
+
- If `revos cubes meta` returns a member name that already contains dots (e.g.
|
|
219
|
+
`cubeA.cubeB.foo`), that **is** the qualified form — paste it through verbatim,
|
|
220
|
+
don't strip the prefix.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Step 3: Run the query
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
revos cubes query --query '<INLINE_JSON>' --json
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
For anything longer than a one-liner, write the JSON to a temp file and pass it with
|
|
231
|
+
`@`:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
cat > /tmp/q.json <<'EOF'
|
|
235
|
+
{
|
|
236
|
+
"measures": ["gold_order_items_enriched.revenue"],
|
|
237
|
+
"timeDimensions": [
|
|
238
|
+
{
|
|
239
|
+
"dimension": "gold_order_items_enriched.order_date",
|
|
240
|
+
"granularity": "month",
|
|
241
|
+
"dateRange": "last 12 months"
|
|
242
|
+
}
|
|
243
|
+
]
|
|
244
|
+
}
|
|
245
|
+
EOF
|
|
246
|
+
revos cubes query --query @/tmp/q.json --json
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The response is the raw Cube.js payload — `data` is the array of rows, each row is a
|
|
250
|
+
flat object keyed by the same member names used in the query.
|
|
251
|
+
|
|
252
|
+
If the API returns an error, surface the message verbatim. Common causes: a member
|
|
253
|
+
name typo (re-check `revos cubes meta`), a cube that hasn't been applied yet
|
|
254
|
+
(`revos status` / `revos apply`), or a `dateRange` against a column that isn't a time
|
|
255
|
+
dimension.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Step 4: Render the result in chat
|
|
260
|
+
|
|
261
|
+
Print **two** blocks in the assistant's reply, both as fenced code so they render
|
|
262
|
+
monospaced:
|
|
263
|
+
|
|
264
|
+
### 4a. ASCII table — always
|
|
265
|
+
|
|
266
|
+
Show every returned row. Right-align numeric columns, left-align everything else.
|
|
267
|
+
Format numbers with thousands separators; round to 2 decimals when not integral.
|
|
268
|
+
Truncate long string values to 32 chars with a trailing `…`.
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
traffic_source count
|
|
272
|
+
───────────────── ───────
|
|
273
|
+
Search 142,318
|
|
274
|
+
Organic 88,204
|
|
275
|
+
Email 41,907
|
|
276
|
+
Facebook 22,015
|
|
277
|
+
Display 9,471
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### 4b. ASCII bar chart — when the shape allows
|
|
281
|
+
|
|
282
|
+
If the query returned exactly one measure plus exactly one dimension (categorical or
|
|
283
|
+
time), draw a horizontal bar chart underneath the table. Scale bars to a width of
|
|
284
|
+
**40 characters** based on the largest value in the result set; use the `█` block
|
|
285
|
+
character for filled cells and a single trailing space.
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
Search ████████████████████████████████████████ 142,318
|
|
289
|
+
Organic ████████████████████████▊ 88,204
|
|
290
|
+
Email ███████████▊ 41,907
|
|
291
|
+
Facebook ██████▏ 22,015
|
|
292
|
+
Display ██▋ 9,471
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Time-series example (oldest → newest, top-to-bottom):
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
2025-06 ███████████▊ € 41,200
|
|
299
|
+
2025-07 █████████████████▏ € 59,800
|
|
300
|
+
2025-08 ████████████████████▋ € 72,400
|
|
301
|
+
…
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Skip the chart (table only) when:
|
|
305
|
+
|
|
306
|
+
- The result set is empty.
|
|
307
|
+
- The query has 2+ measures, 2+ dimensions, or a single scalar value (no dimension).
|
|
308
|
+
- All measure values are zero or null.
|
|
309
|
+
|
|
310
|
+
For a single scalar answer (one row, one column), just say the number in prose
|
|
311
|
+
followed by the one-line table — no chart.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Step 5: Explain the result briefly
|
|
316
|
+
|
|
317
|
+
After the rendered output, add 1–3 short sentences in plain English: what was
|
|
318
|
+
measured, the highest / lowest bucket, and any obvious anomaly (e.g. a missing month,
|
|
319
|
+
a single category dominating the total). Keep it to facts that are visible in the
|
|
320
|
+
table — don't speculate about causes.
|
|
321
|
+
|
|
322
|
+
If the user is likely to want to drill in, suggest **one** concrete follow-up query
|
|
323
|
+
they can ask for next ("Want the same broken down by country?"). Don't list more than
|
|
324
|
+
one — keep the chat tight.
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Rules
|
|
329
|
+
|
|
330
|
+
- Never hardcode cube or member names. Always confirm them via `revos cubes meta`
|
|
331
|
+
before composing the query.
|
|
332
|
+
- Before composing a multi-cube query, scan the `joins:` blocks in `cubes/*.yml`. If
|
|
333
|
+
the cubes you need are connected by more than one path, **pick one explicitly with
|
|
334
|
+
a dotted join hint** instead of letting Cube.js guess.
|
|
335
|
+
- Remember that system cubes (scoring, segments, model overlays) live only in
|
|
336
|
+
`revos cubes meta`, not in `cubes/`. Their joins aren't visible in the YAML —
|
|
337
|
+
only in the meta output.
|
|
338
|
+
- Always render the table; render the chart only when the data shape supports it.
|
|
339
|
+
- Keep `limit` set (default 20). For "top N" questions, set `limit` to N and add
|
|
340
|
+
`order` on the measure.
|
|
341
|
+
- Quote query JSON safely on the shell — prefer a temp file with `@/tmp/q.json` over
|
|
342
|
+
a long inline string with embedded quotes.
|
|
343
|
+
- Don't write the query result to disk or invoke a Python plotter — this skill is
|
|
344
|
+
in-chat only. For static PNG visualizations of the cube graph itself, use
|
|
345
|
+
`visualize-semantic-model`.
|
|
346
|
+
- If `revos cubes meta` returns no cubes, stop and tell the user to run `revos apply`
|
|
347
|
+
first.
|