@graphit/cli 0.1.15 → 0.1.18
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/dist/commands/dashboard.js +79 -3
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/ds.js +24 -0
- package/dist/commands/ds.js.map +1 -1
- package/dist/commands/governance.d.ts +2 -0
- package/dist/commands/governance.js +77 -0
- package/dist/commands/governance.js.map +1 -0
- package/dist/commands/kb.js +76 -3
- package/dist/commands/kb.js.map +1 -1
- package/dist/commands/query.js +73 -2
- package/dist/commands/query.js.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/output/format.d.ts +1 -1
- package/dist/output/format.js +2 -0
- package/dist/output/format.js.map +1 -1
- package/dist/output/styled.d.ts +19 -0
- package/dist/output/styled.js +70 -0
- package/dist/output/styled.js.map +1 -0
- package/package.json +3 -1
- package/skills/graphit/SKILL.md +95 -42
- package/skills/graphit/cursor/graphit-dashboard-planning.mdc +5 -0
- package/skills/graphit/cursor/graphit-kb-traversal.mdc +7 -0
- package/skills/graphit/cursor/graphit-sql-reference.mdc +33 -0
- package/skills/graphit/cursor/graphit-style.mdc +16 -0
- package/skills/graphit/graphit.mdc +83 -10
- package/skills/graphit/references/dashboard-planning.md +24 -0
- package/skills/graphit/references/governance.md +106 -0
- package/skills/graphit/references/graphit-style.md +16 -0
- package/skills/graphit/references/kb-traversal.md +65 -0
- package/skills/graphit/references/sql-reference.md +106 -0
|
@@ -25,6 +25,8 @@ NEVER query the warehouse directly when a cached data source covers the table. D
|
|
|
25
25
|
|
|
26
26
|
Run `graphit ds list` FIRST. If a DS covers your table, use `graphit query "SQL" --ds <id>`. Only use `--warehouse` if NO data source exists and the user approves.
|
|
27
27
|
|
|
28
|
+
**Use the DS name in SQL, not the source table name.** Write `FROM MARKETING_UA_DS`, not `FROM MARKETING_UA`. The DS name is what users see in the platform. DuckDB resolves both, but the DS name keeps SQL consistent with the UI.
|
|
29
|
+
|
|
28
30
|
### 3. EVERY element must have entity wrapping
|
|
29
31
|
Without `data-graphit-*` attributes, elements are invisible to the platform. Every chart, KPI card, table, and text section needs ALL FIVE attributes:
|
|
30
32
|
|
|
@@ -32,7 +34,7 @@ Without `data-graphit-*` attributes, elements are invisible to the platform. Eve
|
|
|
32
34
|
<div data-graphit-id="revenue-trend"
|
|
33
35
|
data-graphit-label="Revenue Trend"
|
|
34
36
|
data-graphit-kb="metric:REVENUE,dimension:REGION,table:ORDERS"
|
|
35
|
-
data-graphit-sql="SELECT region, SUM(revenue) FROM
|
|
37
|
+
data-graphit-sql="SELECT region, SUM(revenue) FROM orders GROUP BY region"
|
|
36
38
|
data-graphit-ds="ds_abc123">
|
|
37
39
|
<!-- chart/KPI/table content here -->
|
|
38
40
|
</div>
|
|
@@ -42,17 +44,20 @@ Without `data-graphit-*` attributes, elements are invisible to the platform. Eve
|
|
|
42
44
|
|-----------|--------|---------|
|
|
43
45
|
| `data-graphit-id` | Unique kebab-case | `"spend-by-source"` |
|
|
44
46
|
| `data-graphit-label` | Human-readable name | `"Ad Spend by Source"` |
|
|
45
|
-
| `data-graphit-kb` | `type:NAME` comma-separated | `"metric:CPI,dimension:MEDIA_SOURCE,table:
|
|
47
|
+
| `data-graphit-kb` | `type:NAME` comma-separated | `"metric:CPI,dimension:MEDIA_SOURCE,table:MARKETING_UA_DS"` |
|
|
46
48
|
| `data-graphit-sql` | Executable SQL (HTML-encode `<>&"`) | `"SELECT ..."` |
|
|
47
49
|
| `data-graphit-ds` | Data source ID from `graphit ds list` | `"ds_abc123"` |
|
|
48
50
|
|
|
49
51
|
KB types: `metric`, `dimension`, `table`, `rule`. Names are UPPER_SNAKE_CASE matching the KB exactly. **Names are unique per type per org** - creating a duplicate name will fail. Check `graphit kb list <type>` first; update the existing asset instead of creating a new one.
|
|
50
52
|
|
|
51
|
-
**SQL must be executable.** The platform runs `data-graphit-sql` against the data source when a user opens the entity's details panel. It MUST use the real DS table name and only columns that exist in the DS - never invented summary columns, CTE aliases, JS variable names, or prose. If the chart's resolve call uses a CTE, put the full WITH query. If JS builds SQL dynamically, store one representative executable variant (e.g. the default date range).
|
|
53
|
+
**SQL must be complete and executable.** The platform runs `data-graphit-sql` against the data source when a user opens the entity's details panel. Write the full query from the `graphit.resolve()` call - never abbreviate, truncate, or use placeholders (`FROM ...`, `SELECT ...`, ellipsis). It MUST use the real DS table name and only columns that exist in the DS - never invented summary columns, CTE aliases, JS variable names, or prose. If the chart's resolve call uses a CTE, put the full WITH query. If JS builds SQL dynamically, store one representative executable variant (e.g. the default date range).
|
|
52
54
|
|
|
53
55
|
**Label = visible title.** The `data-graphit-label` MUST match the card's visible heading exactly.
|
|
54
56
|
|
|
55
|
-
### 4.
|
|
57
|
+
### 4. Shared dashboards require Edit mode on the platform
|
|
58
|
+
Dashboards shared with teams are protected by an editing session lock. The CLI cannot acquire editing sessions - mutations (`dashboard update-html`, `dashboard update-entity`) on a shared dashboard will fail with 423. Tell the user to open the dashboard on the Graphit platform and click **Edit** first. Private dashboards are unaffected.
|
|
59
|
+
|
|
60
|
+
### 5. ALWAYS use graphit.resolve() for live data
|
|
56
61
|
NEVER embed query results as static JS variables. The dashboard iframe provides `graphit.resolve()` which fetches live data from cached data sources on every page load.
|
|
57
62
|
|
|
58
63
|
**Wrong:** Embedding results as `const data = [{...}, ...]` in the HTML.
|
|
@@ -60,6 +65,38 @@ NEVER embed query results as static JS variables. The dashboard iframe provides
|
|
|
60
65
|
|
|
61
66
|
---
|
|
62
67
|
|
|
68
|
+
## Presenting Results to the User
|
|
69
|
+
|
|
70
|
+
You are the rendering layer - format and present every CLI result using markdown. Never silently consume output.
|
|
71
|
+
|
|
72
|
+
**Formatting fundamentals:** Bold every KB asset/table/DS name (**CPI**, **MARKETING_UA_DS**). Markdown tables for tabular data. Code blocks (```sql) for every SQL query. Format numbers: commas (12,400), $ for currency, % for rates. Narrate between steps.
|
|
73
|
+
|
|
74
|
+
**After `graphit query`** - ground in KB, show five sections:
|
|
75
|
+
1. **KB Assets:** bold names of all referenced metrics/dimensions/tables
|
|
76
|
+
2. **Query:** original SQL with `{{metric:X}}` references in ```sql block
|
|
77
|
+
3. **Resolved SQL:** expanded SQL (always use `--verbose`) in ```sql block
|
|
78
|
+
4. **Results:** markdown table, row count, DS ID
|
|
79
|
+
5. **Governance:** tier, KB refs, rules enforced (**bold names**)
|
|
80
|
+
- For ad-hoc queries: show query + results + governance, suggest KB reference equivalent
|
|
81
|
+
|
|
82
|
+
**After `graphit kb list`** - count summary + table with bold names:
|
|
83
|
+
- `**12 metrics** across 3 tables:` then table with **bold name**, table, calculation, params
|
|
84
|
+
- Footer summarizing parameterized vs pre-baked
|
|
85
|
+
|
|
86
|
+
**After `graphit kb get`** - entity heading + key-value table:
|
|
87
|
+
- `### **CPI** (metric, verified)` with description in italics
|
|
88
|
+
- Key-value table: Table, Calculation, Parameters, Topics, Dimensions
|
|
89
|
+
|
|
90
|
+
**After `graphit kb search`** - result count + table with type column
|
|
91
|
+
**After `graphit kb explore`** - tree structure: Asset > Tables > Dimensions > Rules
|
|
92
|
+
**After `graphit ds list`** - table with Name, ID, Rows, Status, Governed + recommend which DS to use
|
|
93
|
+
**After `graphit governance status`** - mode + conformance table (Governed/Ad-hoc/Blocked with counts and %)
|
|
94
|
+
**After errors** - bold error message + actionable fix suggestion
|
|
95
|
+
|
|
96
|
+
**Never:** run queries silently, present raw JSON, skip governance footer, omit SQL code blocks, show tables without bold asset names.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
63
100
|
## Workflow: Dashboard Build
|
|
64
101
|
|
|
65
102
|
1. **Ask the user** what dashboard they want.
|
|
@@ -81,6 +118,27 @@ When the user wants to build or populate their KB from files, schemas, or scratc
|
|
|
81
118
|
5. **Execute top-down.** Create in order: domains, topics, metrics (with --topics), dimensions, rules, synonyms, relationships, then attachments (domain->table, secondary_tables).
|
|
82
119
|
6. **Verify.** Re-traverse with `graphit kb explore domain <NAME>`, confirm counts match plan.
|
|
83
120
|
|
|
121
|
+
## Query Governance and Reference Syntax
|
|
122
|
+
|
|
123
|
+
Use KB references for governed queries:
|
|
124
|
+
- `{{metric:NAME}}` - expands to metric calculation, e.g. `{{metric:CPI}}`
|
|
125
|
+
- `{{metric:NAME(K=V)}}` - parameterized metric, e.g. `{{metric:ARPU(DAY=7)}}`
|
|
126
|
+
- `{{dim:NAME}}` - expands to dimension expression
|
|
127
|
+
- `{{metric_raw:NAME}}` - raw expression (no aggregate)
|
|
128
|
+
|
|
129
|
+
**Parameterized metrics:** Some metrics require parameters (check `graphit kb list metric` - `params` column). Use `{{metric:ARPU(DAY=7)}}` to resolve `${PARAM:DAY}`. Pre-baked variants (ARPU_D7, ROAS_D30) need no parameters.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
graphit query "SELECT {{dim:INSTALL_MONTH}}, {{metric:CPI}} as cpi FROM MARKETING_UA_DS GROUP BY 1" --ds ds_abc123
|
|
133
|
+
graphit query "SELECT {{metric:ARPU(DAY=7)}} as arpu FROM MARKETING_UA_DS" --ds ds_abc123 # parameterized
|
|
134
|
+
graphit query "SELECT * FROM events" --ds ds_123 --override-rules EXCLUDE_RETARGETING # bypass rule
|
|
135
|
+
graphit governance status # view mode and conformance
|
|
136
|
+
graphit governance set warn # change mode (admin)
|
|
137
|
+
graphit ds update <id> --governed-mode on # enable governance on DS
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Reference syntax in `graphit.resolve()` calls works the same way - server expands at query time. Trust tiers: **governed** (KB refs), **verified** (matches KB), **ad-hoc** (inline). Strict mode blocks ad-hoc on governed DSes.
|
|
141
|
+
|
|
84
142
|
## graphit.resolve() - Live Data API
|
|
85
143
|
|
|
86
144
|
```js
|
|
@@ -93,14 +151,27 @@ const result = await graphit.resolve({
|
|
|
93
151
|
// Returns: { columns: string[], data: object[], rowCount: number, truncated: boolean }
|
|
94
152
|
```
|
|
95
153
|
|
|
96
|
-
**Error handling:** Rejects on timeout (
|
|
154
|
+
**Error handling:** Rejects on timeout (120s), bad SQL, or invalid data source ID. Wrap in try/catch.
|
|
155
|
+
|
|
156
|
+
### First-paint loading state
|
|
157
|
+
|
|
158
|
+
The HTML paints before the SDK connects, so bake a pure-CSS spinner overlay into the page; the SDK adopts it and removes it when that element's `graphit.resolve()` settles (success or error). Add once to the page `<style>`:
|
|
159
|
+
|
|
160
|
+
```css
|
|
161
|
+
@keyframes gh-spin{to{transform:rotate(360deg)}}
|
|
162
|
+
.gh-loading{position:relative;min-height:120px}
|
|
163
|
+
.gh-loading-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;z-index:9998;backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);background:color-mix(in srgb,var(--graphit-surface-raised,#fff) 50%,transparent);border-radius:inherit}
|
|
164
|
+
.gh-loading-spin{animation:gh-spin .7s linear infinite}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Add the overlay inside EVERY element you pass as `target:` - and ONLY those elements (a static text/title section with no resolve call would spin forever). Keep the class names exactly as shown; they are the contract the SDK uses to adopt and remove the overlay. NEVER write text placeholders ("Loading...", "Fetching data...") - they never animate and make slow loads look stuck. See the canonical pattern below for the overlay markup.
|
|
97
168
|
|
|
98
169
|
### Runtime chart helpers
|
|
99
170
|
|
|
100
171
|
| Helper | Usage |
|
|
101
172
|
|---|---|
|
|
102
173
|
| `graphit.chart(el, {type, data, x, y, ...})` | Bar, line, area, donut, scatter, stacked-bar, heatmap, funnel, gauge, sparkline |
|
|
103
|
-
| `graphit.table(el, {data, columns?,
|
|
174
|
+
| `graphit.table(el, {data, columns?, maxRows?})` | Styled HTML table. `columns` is a `string[]` of data keys (NOT objects), defaults to `Object.keys(data[0])` |
|
|
104
175
|
| `graphit.kpi(el, {value, label?, format?})` | KPI card with optional delta |
|
|
105
176
|
|
|
106
177
|
`graphit.chart` types: `"bar"`, `"line"`, `"area"`, `"donut"` (alias `"pie"`), `"scatter"` (alias `"bubble"`), `"stacked-bar"` (alias `"stacked"`), `"heatmap"`, `"funnel"`, `"gauge"`, `"sparkline"`. Config: `x` (category field), `y` (value field), `series` (group-by), `title`, `height` (140-900px), `valueFormat` (`"currency"` | `"percent"` | `"number"`), `colors` (array). Scatter adds: `size`, `label`. Gauge adds: `min`, `max`, `format`. Sparkline adds: `width`, `showValue`.
|
|
@@ -110,15 +181,17 @@ const result = await graphit.resolve({
|
|
|
110
181
|
```html
|
|
111
182
|
<div data-graphit-id="spend-by-source"
|
|
112
183
|
data-graphit-label="Ad Spend by Source"
|
|
113
|
-
data-graphit-kb="metric:CPI,dimension:MEDIA_SOURCE,table:
|
|
114
|
-
data-graphit-sql="SELECT MEDIA_SOURCE, SUM(APPSFLYER_COST) as spend FROM
|
|
184
|
+
data-graphit-kb="metric:CPI,dimension:MEDIA_SOURCE,table:MARKETING_UA_DS"
|
|
185
|
+
data-graphit-sql="SELECT MEDIA_SOURCE, SUM(APPSFLYER_COST) as spend FROM MARKETING_UA_DS GROUP BY MEDIA_SOURCE ORDER BY spend DESC"
|
|
115
186
|
data-graphit-ds="ds_abc123">
|
|
116
|
-
<div id="spend-chart"
|
|
187
|
+
<div id="spend-chart" class="gh-loading">
|
|
188
|
+
<div class="gh-loading-overlay"><svg class="gh-loading-spin" width="24" height="24" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="var(--graphit-border,#e5e5e5)" stroke-width="2.5"/><path d="M12 2a10 10 0 0 1 10 10" stroke="var(--graphit-accent,#4DB6AC)" stroke-width="2.5" stroke-linecap="round"/></svg></div>
|
|
189
|
+
</div>
|
|
117
190
|
</div>
|
|
118
191
|
<script>
|
|
119
192
|
(async function() {
|
|
120
193
|
var r = await graphit.resolve({
|
|
121
|
-
sql: "SELECT MEDIA_SOURCE, SUM(APPSFLYER_COST) as spend FROM
|
|
194
|
+
sql: "SELECT MEDIA_SOURCE, SUM(APPSFLYER_COST) as spend FROM MARKETING_UA_DS GROUP BY MEDIA_SOURCE ORDER BY spend DESC",
|
|
122
195
|
dataSourceId: "ds_abc123",
|
|
123
196
|
target: "#spend-chart"
|
|
124
197
|
});
|
|
@@ -92,3 +92,27 @@ Before generating the HTML:
|
|
|
92
92
|
- [ ] 3+ chart types for 4+ graphs
|
|
93
93
|
- [ ] Every KPI has definition + baseline + direction
|
|
94
94
|
- [ ] No anti-patterns present
|
|
95
|
+
|
|
96
|
+
## Presenting Dashboard & Connector Results
|
|
97
|
+
|
|
98
|
+
**After `graphit dashboard list`:**
|
|
99
|
+
|
|
100
|
+
~~~
|
|
101
|
+
**4 dashboards:**
|
|
102
|
+
|
|
103
|
+
| Name | ID | URL |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| **UA Command Center** | dash_abc | https://app.graphit-app.com/custom-dashboard/dash_abc |
|
|
106
|
+
| **Player Quality** | dash_def | https://app.graphit-app.com/custom-dashboard/dash_def |
|
|
107
|
+
~~~
|
|
108
|
+
|
|
109
|
+
**After `graphit connector list`:**
|
|
110
|
+
|
|
111
|
+
~~~
|
|
112
|
+
**2 connections:**
|
|
113
|
+
|
|
114
|
+
| Name | Account | Auth |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| **prod-snowflake** | xy12345.us-east-1 | OAuth |
|
|
117
|
+
| **staging-sf** | ab67890.us-east-1 | Keypair |
|
|
118
|
+
~~~
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Query Governance
|
|
2
|
+
|
|
3
|
+
Server-side governance ensures consistent, auditable queries across all channels.
|
|
4
|
+
|
|
5
|
+
## Reference Syntax
|
|
6
|
+
|
|
7
|
+
Use KB references instead of inline formulas for governed queries:
|
|
8
|
+
|
|
9
|
+
```sql
|
|
10
|
+
-- Governed: server expands to KB-defined formulas
|
|
11
|
+
SELECT {{dim:INSTALL_MONTH}}, {{metric:CPI}} as cpi
|
|
12
|
+
FROM MARKETING_UA_DS
|
|
13
|
+
GROUP BY 1
|
|
14
|
+
|
|
15
|
+
-- Parameterized metric (ARPU requires DAY parameter)
|
|
16
|
+
SELECT {{metric:ARPU(DAY=7)}} as arpu FROM MARKETING_UA_DS
|
|
17
|
+
|
|
18
|
+
-- Raw expression (no outer aggregate)
|
|
19
|
+
SELECT {{metric_raw:REVENUE}} FROM orders
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The server compiles references to actual expressions, injects rule constraints, stamps trust tier, and caches results. Reference syntax works in both `graphit query` and `graphit.resolve()`.
|
|
23
|
+
|
|
24
|
+
**Parameterized metrics:** Some metrics (ARPU, ROAS, RETENTION) require parameters. Check `graphit kb list metric` - the `params` column shows required names. Pre-baked variants (ARPU_D7, ROAS_D30) have values hardcoded and need no parameters. Omitting required parameters returns a clear error with the exact syntax to use.
|
|
25
|
+
|
|
26
|
+
## Trust Tiers
|
|
27
|
+
|
|
28
|
+
| Tier | Meaning | Badge |
|
|
29
|
+
|------|---------|-------|
|
|
30
|
+
| **governed** | Used `{{metric:X}}` / `{{dim:X}}` references | Teal dot |
|
|
31
|
+
| **verified** | Raw SQL matches KB definitions (AST matching) | Amber dot |
|
|
32
|
+
| **ad_hoc** | Inline formulas, no KB match | Gray dot |
|
|
33
|
+
|
|
34
|
+
## Governance Modes
|
|
35
|
+
|
|
36
|
+
| Mode | Behavior |
|
|
37
|
+
|------|----------|
|
|
38
|
+
| `observe` | All queries pass, tiers are tracked |
|
|
39
|
+
| `warn` | Ad-hoc queries on governed DSes produce warnings |
|
|
40
|
+
| `strict` | Ad-hoc queries on governed DSes are blocked |
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
graphit governance status # View mode and conformance stats
|
|
44
|
+
graphit governance set warn # Change mode (admin only)
|
|
45
|
+
graphit governance audit # View audit log
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Enforceable Rules
|
|
49
|
+
|
|
50
|
+
Rules with typed constraints are enforced automatically via SQL injection:
|
|
51
|
+
|
|
52
|
+
| Type | What it does |
|
|
53
|
+
|------|-------------|
|
|
54
|
+
| `required_where` | Injects a WHERE predicate |
|
|
55
|
+
| `forbidden_column` | NULLifies a column in SELECT |
|
|
56
|
+
| `value_restriction` | Restricts column to allowed values |
|
|
57
|
+
| `required_filter` | Validates column appears in WHERE |
|
|
58
|
+
| `required_aggregation` | Validates GROUP BY includes column |
|
|
59
|
+
|
|
60
|
+
User-context variables (`${user.team_id}`, `${user.email}`) are resolved server-side for row-level security.
|
|
61
|
+
|
|
62
|
+
## Override Flow
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Override a specific rule
|
|
66
|
+
graphit query "SELECT * FROM events" --ds ds_123 --override-rules EXCLUDE_RETARGETING
|
|
67
|
+
|
|
68
|
+
# Multiple overrides
|
|
69
|
+
graphit query "SELECT * FROM events" --ds ds_123 --override-rules RULE1 RULE2
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Override is checked against the rule's `override_policy` (anyone/analyst_only/admin_only/never) and the user's role. Overrides are always logged to the audit trail.
|
|
73
|
+
|
|
74
|
+
## Per-DS Settings
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
graphit ds update <id> --governed-mode on # Enable governed mode
|
|
78
|
+
graphit ds update <id> --governed-mode off # Disable
|
|
79
|
+
graphit ds update <id> --max-rows 10000 # Set row cap
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Presenting Governance Results
|
|
83
|
+
|
|
84
|
+
**After `graphit governance status`:**
|
|
85
|
+
|
|
86
|
+
~~~
|
|
87
|
+
**Governance mode:** strict
|
|
88
|
+
|
|
89
|
+
**7-day conformance:**
|
|
90
|
+
|
|
91
|
+
| Tier | Queries | Share |
|
|
92
|
+
|---|---:|---:|
|
|
93
|
+
| Governed | 847 | 72% |
|
|
94
|
+
| Ad-hoc | 328 | 28% |
|
|
95
|
+
| Blocked | 12 | 1% |
|
|
96
|
+
|
|
97
|
+
5 active rules across 3 governed data sources.
|
|
98
|
+
~~~
|
|
99
|
+
|
|
100
|
+
**After governance errors:**
|
|
101
|
+
|
|
102
|
+
~~~
|
|
103
|
+
**Error:** Query blocked by governance.
|
|
104
|
+
|
|
105
|
+
Rule **EXCLUDE_ORGANIC** (override policy: `never`) cannot be overridden. Contact your admin to change the policy, or rewrite the query to include the required filter.
|
|
106
|
+
~~~
|
|
@@ -73,6 +73,12 @@ Use the system font stack everywhere: `-apple-system, BlinkMacSystemFont, 'Segoe
|
|
|
73
73
|
box-shadow:0 1px 3px rgba(0,0,0,0.08); }
|
|
74
74
|
.card h3 { font-size:12px; font-weight:600; color:var(--graphit-fg-subtle); text-transform:uppercase;
|
|
75
75
|
letter-spacing:0.05em; margin-bottom:16px; }
|
|
76
|
+
@keyframes gh-spin{to{transform:rotate(360deg)}}
|
|
77
|
+
.gh-loading { position:relative; min-height:120px; }
|
|
78
|
+
.gh-loading-overlay { position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
|
|
79
|
+
z-index:9998; backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px);
|
|
80
|
+
background:color-mix(in srgb,var(--graphit-surface-raised,#fff) 50%,transparent); border-radius:inherit; }
|
|
81
|
+
.gh-loading-spin { animation:gh-spin .7s linear infinite; }
|
|
76
82
|
@media(max-width:900px) {
|
|
77
83
|
.kpi-grid { grid-template-columns:repeat(2,1fr); }
|
|
78
84
|
.charts-grid { grid-template-columns:1fr; }
|
|
@@ -112,6 +118,16 @@ Use the system font stack everywhere: `-apple-system, BlinkMacSystemFont, 'Segoe
|
|
|
112
118
|
</style>
|
|
113
119
|
```
|
|
114
120
|
|
|
121
|
+
### Loading State
|
|
115
122
|
|
|
123
|
+
Bake this overlay inside every element passed as `target:` to `graphit.resolve()` - and only those (static text/title cards would spin forever). It shows from the first paint, before the SDK connects; the SDK removes it when the resolve settles. Class names are the SDK contract - keep them exactly:
|
|
124
|
+
|
|
125
|
+
```html
|
|
126
|
+
<div id="spend-chart" class="gh-loading">
|
|
127
|
+
<div class="gh-loading-overlay"><svg class="gh-loading-spin" width="24" height="24" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="var(--graphit-border,#e5e5e5)" stroke-width="2.5"/><path d="M12 2a10 10 0 0 1 10 10" stroke="var(--graphit-accent,#4DB6AC)" stroke-width="2.5" stroke-linecap="round"/></svg></div>
|
|
128
|
+
</div>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Never write "Loading..." text placeholders - they don't animate and make slow loads look stuck.
|
|
116
132
|
|
|
117
133
|
For inline chart implementations (bar, line, donut, heatmap, funnel, sparkline, gauge, stacked bar, multi-series), see `chart-patterns.md`.
|
|
@@ -56,3 +56,68 @@ Use `kb_explore` with max_depth=2 and no edge_type filter. Returns the table's d
|
|
|
56
56
|
| 3 (max) | Full neighborhood. Use sparingly - can return many results for connected tables. |
|
|
57
57
|
|
|
58
58
|
Start at depth 1. Only increase if the user's question requires it or the initial results are insufficient.
|
|
59
|
+
|
|
60
|
+
## Presenting KB Results
|
|
61
|
+
|
|
62
|
+
**After `graphit kb list <type>`** - count summary + table with bold names:
|
|
63
|
+
|
|
64
|
+
~~~
|
|
65
|
+
**12 metrics** across 3 tables:
|
|
66
|
+
|
|
67
|
+
| Metric | Table | Calculation | Params |
|
|
68
|
+
|---|---|---|---|
|
|
69
|
+
| **CPI** | **MARKETING_UA** | `SUM(spend)/SUM(installs)` | - |
|
|
70
|
+
| **ARPU** | **MARKETING_UA** | `SUM(revenue)/COUNT(...)` | DAY |
|
|
71
|
+
| **RETENTION** | **PLAYER_QUALITY** | `COUNT(CASE WHEN ...)` | DAY |
|
|
72
|
+
| ... | | | |
|
|
73
|
+
|
|
74
|
+
4 parameterized (need `(DAY=N)` syntax), 8 pre-baked.
|
|
75
|
+
~~~
|
|
76
|
+
|
|
77
|
+
Adapt columns per type: dimensions include semantic type, rules include constraint count, domains include asset count.
|
|
78
|
+
|
|
79
|
+
**After `graphit kb get <type> <name>`** - entity heading + key-value table:
|
|
80
|
+
|
|
81
|
+
~~~
|
|
82
|
+
### **CPI** (metric, verified)
|
|
83
|
+
|
|
84
|
+
*Cost per install*
|
|
85
|
+
|
|
86
|
+
| | |
|
|
87
|
+
|---:|---|
|
|
88
|
+
| **Table** | **MARKETING_UA** |
|
|
89
|
+
| **Calculation** | `SUM(spend) / SUM(installs)` |
|
|
90
|
+
| **Parameters** | none |
|
|
91
|
+
| **Topics** | ACQUISITION, SPEND |
|
|
92
|
+
| **Default dims** | MEDIA_SOURCE, CAMPAIGN_NAME |
|
|
93
|
+
~~~
|
|
94
|
+
|
|
95
|
+
Adapt fields per type. Rules: content, constraints, apply-on, override policy. Dimensions: expression, semantic type, output type.
|
|
96
|
+
|
|
97
|
+
**After `graphit kb search`** - result count + table with type column:
|
|
98
|
+
|
|
99
|
+
~~~
|
|
100
|
+
**5 results** for "revenue":
|
|
101
|
+
|
|
102
|
+
| Type | Name | Description |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| metric | **TOTAL_REVENUE** | Total revenue across all channels |
|
|
105
|
+
| dimension | **REVENUE_BUCKET** | Revenue range segmentation |
|
|
106
|
+
| synonym | GMV | Maps to **TOTAL_REVENUE** (metric) |
|
|
107
|
+
~~~
|
|
108
|
+
|
|
109
|
+
**After `graphit kb explore`** - tree with bold names, indented by level:
|
|
110
|
+
|
|
111
|
+
~~~
|
|
112
|
+
**CPI** (metric) on **MARKETING_UA**:
|
|
113
|
+
- **Calculation:** `SUM(spend) / SUM(installs)`
|
|
114
|
+
- **Tables:** **MARKETING_UA**, **MARKETING_UA_6MO** (secondary)
|
|
115
|
+
- **Related dimensions (8):**
|
|
116
|
+
- **MEDIA_SOURCE** - `media_source` (categorical)
|
|
117
|
+
- **CAMPAIGN_NAME** - `campaign_name` (categorical)
|
|
118
|
+
- ...
|
|
119
|
+
- **Rules:** **EXCLUDE_ORGANIC** (filters organic installs)
|
|
120
|
+
- **Domain:** MARKETING
|
|
121
|
+
~~~
|
|
122
|
+
|
|
123
|
+
For domain exploration, show Domain > Table > Asset hierarchy as a tree.
|
|
@@ -111,6 +111,24 @@ SELECT UNNEST(generate_series(
|
|
|
111
111
|
|
|
112
112
|
The outer SELECT can ONLY reference columns the subquery exposes (its aliases). Base-table columns aggregated away in the subquery do NOT exist at the outer level.
|
|
113
113
|
|
|
114
|
+
## Cache-Friendly SQL (Canvas Resolve)
|
|
115
|
+
|
|
116
|
+
Canvas `graphit.resolve()` queries that follow these shapes serve from a semantic cache in ~10ms on filter changes instead of a full DuckDB recompute (5-37s on wide data sources). Write resolve SQL in this style by default.
|
|
117
|
+
|
|
118
|
+
**Shape rules:**
|
|
119
|
+
- Single table (no JOIN/UNION)
|
|
120
|
+
- WHERE as flat AND of `column = literal`, `column IN (...)`, `column BETWEEN ... AND ...` conjuncts
|
|
121
|
+
- Bare aggregates only: `SUM(col)`, `COUNT(*)`, `MIN(col)`, `MAX(col)` - no wrapping functions (`ROUND(SUM(x))`), no aggregate arithmetic (`SUM(a)/NULLIF(SUM(b),0)`), no `AVG` (v2)
|
|
122
|
+
- Literal dates (`>= '2026-01-01'`), never `CURRENT_DATE` expressions
|
|
123
|
+
- GROUP BY column names or ordinals; ORDER BY / LIMIT allowed (outer only)
|
|
124
|
+
- CTEs are fine when the CTE body follows the same rules
|
|
125
|
+
|
|
126
|
+
**Shapes that skip the cache** (fall back to normal execution, still correct):
|
|
127
|
+
- `COUNT(DISTINCT x)`, window functions, HAVING, QUALIFY
|
|
128
|
+
- OR / NOT in WHERE
|
|
129
|
+
- Ratio metrics (`SUM(a)/NULLIF(SUM(b),0)`) - compute client-side or use two resolves
|
|
130
|
+
- `CURRENT_DATE`-relative predicates
|
|
131
|
+
|
|
114
132
|
## Data Source Routing
|
|
115
133
|
|
|
116
134
|
| Situation | Command | Speed |
|
|
@@ -119,3 +137,91 @@ The outer SELECT can ONLY reference columns the subquery exposes (its aliases).
|
|
|
119
137
|
| No data source | `graphit query "SQL" --warehouse --connection <id>` | ~10s, Snowflake |
|
|
120
138
|
|
|
121
139
|
Always prefer cached data sources. Check with `graphit ds list`. If no data source covers the table, suggest creating one for future speed.
|
|
140
|
+
|
|
141
|
+
## Presenting Query Results
|
|
142
|
+
|
|
143
|
+
After every `graphit query`, present results grounded in the KB. Always show which KB assets were used - this is what makes governed queries valuable.
|
|
144
|
+
|
|
145
|
+
**When using KB reference syntax** (`{{metric:X}}`, `{{dim:X}}`), show all five sections:
|
|
146
|
+
|
|
147
|
+
~~~
|
|
148
|
+
**KB Assets:** dimension **CAMPAIGN_CATEGORY**, metric **TOTAL_INSTALLS**, metric **CPI**, table **MARKETING_UA_DS**
|
|
149
|
+
|
|
150
|
+
**Query:**
|
|
151
|
+
```sql
|
|
152
|
+
SELECT
|
|
153
|
+
{{dim:CAMPAIGN_CATEGORY}} AS category,
|
|
154
|
+
{{metric:TOTAL_INSTALLS}} AS installs,
|
|
155
|
+
{{metric:CPI}} AS cpi
|
|
156
|
+
FROM MARKETING_UA_DS
|
|
157
|
+
WHERE ACTIVITY_TIME >= '2026-01-01'
|
|
158
|
+
GROUP BY {{dim:CAMPAIGN_CATEGORY}}
|
|
159
|
+
ORDER BY installs DESC
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Resolved SQL:**
|
|
163
|
+
```sql
|
|
164
|
+
SELECT
|
|
165
|
+
CASE WHEN IS_RETARGETING THEN 'Retargeting'
|
|
166
|
+
WHEN IS_CTV THEN 'Connected TV'
|
|
167
|
+
ELSE 'User Acquisition' END AS category,
|
|
168
|
+
SUM(INSTALLS) AS installs,
|
|
169
|
+
SUM(APPSFLYER_COST) / NULLIF(SUM(INSTALLS), 0) AS cpi
|
|
170
|
+
FROM MARKETING_UA_DS
|
|
171
|
+
WHERE ACTIVITY_TIME >= '2026-01-01'
|
|
172
|
+
GROUP BY 1
|
|
173
|
+
ORDER BY installs DESC
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Results** (3 rows via **ds_abc123**):
|
|
177
|
+
|
|
178
|
+
| Category | Installs | CPI |
|
|
179
|
+
|---|---:|---:|
|
|
180
|
+
| User Acquisition | 80,605 | $0.79 |
|
|
181
|
+
| Retargeting | 12,300 | $1.24 |
|
|
182
|
+
| Connected TV | 3,100 | $2.80 |
|
|
183
|
+
|
|
184
|
+
**Governance:** governed - 3 KB refs, 2 rules enforced (**EXCLUDE_ORGANIC**, **MIN_SPEND**). Max rows: 1,000.
|
|
185
|
+
~~~
|
|
186
|
+
|
|
187
|
+
**When using inline SQL** (no `{{metric:X}}`), show query + results + governance:
|
|
188
|
+
|
|
189
|
+
~~~
|
|
190
|
+
**Query:**
|
|
191
|
+
```sql
|
|
192
|
+
SELECT media_source, SUM(spend) AS spend FROM MARKETING_UA_DS GROUP BY 1
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Results** (6 rows via **ds_abc123**):
|
|
196
|
+
|
|
197
|
+
| Media Source | Spend |
|
|
198
|
+
|---|---:|
|
|
199
|
+
| Facebook | $42,100 |
|
|
200
|
+
| Google UAC | $38,500 |
|
|
201
|
+
|
|
202
|
+
**Governance:** ad-hoc - 0 KB refs. Consider using `{{metric:TOTAL_SPEND}}` for governed tier.
|
|
203
|
+
~~~
|
|
204
|
+
|
|
205
|
+
For ad-hoc queries, suggest the KB reference equivalent when one exists. This nudges users toward governed queries.
|
|
206
|
+
|
|
207
|
+
**Always use `--verbose`** to get the resolved SQL. If the user didn't pass it, re-run with `--verbose` so you can show both the reference query and the expanded SQL.
|
|
208
|
+
|
|
209
|
+
Zero rows: explain what you checked and hypothesize why (wrong date range, filter too strict, table empty).
|
|
210
|
+
|
|
211
|
+
## Presenting Data Source Results
|
|
212
|
+
|
|
213
|
+
After `graphit ds list`:
|
|
214
|
+
|
|
215
|
+
~~~
|
|
216
|
+
**3 data sources:**
|
|
217
|
+
|
|
218
|
+
| Name | ID | Rows | Status | Governed |
|
|
219
|
+
|---|---|---:|---|---|
|
|
220
|
+
| **MARKETING_UA_DS** | ds_abc123 | 1,247,832 | active | yes |
|
|
221
|
+
| **PLAYER_QUALITY** | ds_def456 | 892,104 | active | no |
|
|
222
|
+
| **REVENUE_EVENTS** | ds_ghi789 | 3,412,006 | stale | yes |
|
|
223
|
+
|
|
224
|
+
Using **MARKETING_UA_DS** (ds_abc123) which covers spend, installs, and ROAS columns.
|
|
225
|
+
~~~
|
|
226
|
+
|
|
227
|
+
End with a recommendation of which DS to use, or note if none covers the needed table.
|