@graphit/cli 0.1.51 → 0.1.106
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/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +3 -2
- package/README.md +99 -0
- package/bin/graphit +45 -0
- package/bin/graphit.cmd +8 -0
- package/bin/graphit.ps1 +38 -0
- package/dist/api/client.js +11 -0
- package/dist/api/client.js.map +1 -1
- package/dist/commands/auth.js +14 -6
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/ds-poll.d.ts +8 -0
- package/dist/commands/ds-poll.js +53 -0
- package/dist/commands/ds-poll.js.map +1 -0
- package/dist/commands/ds.js +113 -57
- package/dist/commands/ds.js.map +1 -1
- package/dist/commands/governance.js +15 -11
- package/dist/commands/governance.js.map +1 -1
- package/dist/commands/kb.js +14 -4
- package/dist/commands/kb.js.map +1 -1
- package/dist/commands/query.js +9 -7
- package/dist/commands/query.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/output/format.d.ts +1 -0
- package/dist/output/format.js +6 -0
- package/dist/output/format.js.map +1 -1
- package/dist/output/table.d.ts +1 -0
- package/dist/output/table.js +20 -3
- package/dist/output/table.js.map +1 -1
- package/dist/skill-version.d.ts +12 -0
- package/dist/skill-version.js +21 -0
- package/dist/skill-version.js.map +1 -1
- package/dist/update-check.d.ts +19 -0
- package/dist/update-check.js +105 -12
- package/dist/update-check.js.map +1 -1
- package/hooks/hooks.json +13 -0
- package/package.json +11 -3
- package/scripts/block-legacy-setup.mjs +90 -0
- package/scripts/generate-commands-doc.mjs +182 -0
- package/scripts/plugin-status.mjs +179 -29
- package/scripts/sync-plugin-version.mjs +26 -4
- package/skills/graphit/SKILL.md +172 -433
- package/skills/graphit/VERSION.json +1 -1
- package/skills/graphit/cursor/README.md +11 -0
- package/skills/graphit/graphit.mdc +3 -1
- package/skills/graphit/references/dashboard-planning.md +1 -7
- package/skills/graphit/references/data-sources.md +30 -1
- package/skills/graphit/references/filters-advanced.md +61 -0
- package/skills/graphit/references/filters.md +27 -93
- package/skills/graphit/references/governance.md +49 -66
- package/skills/graphit/references/graphit-style.md +6 -18
- package/skills/graphit/references/kb-actions.md +44 -52
- package/skills/graphit/references/kb-discovery.md +10 -13
- package/skills/graphit/references/kb-structure.md +10 -8
- package/skills/graphit/references/kb-traversal.md +16 -16
- package/skills/graphit/references/operations.md +115 -0
- package/skills/graphit/references/parameterized-metrics.md +32 -44
- package/skills/graphit/references/runtime.md +172 -0
- package/skills/graphit/references/sql-reference.md +5 -42
- package/skills/graphit/references/domain-lenses.md +0 -116
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Cursor references - FROZEN
|
|
2
|
+
|
|
3
|
+
These `graphit-*.mdc` files are the Cursor mirror of the Graphit CLI skill. They are FROZEN.
|
|
4
|
+
|
|
5
|
+
- They are kept on disk so existing Cursor users keep working.
|
|
6
|
+
- They are no longer synced from `SKILL.md` + `references/`, and the version stamper no longer touches them.
|
|
7
|
+
- They may be stale relative to the live skill.
|
|
8
|
+
|
|
9
|
+
The source of truth for the Graphit CLI skill is `cli/skills/graphit/SKILL.md` plus `cli/skills/graphit/references/`. Maintained platforms are Claude Code and Codex.
|
|
10
|
+
|
|
11
|
+
Do not edit these files to fix a CLI behavior. Fix it in `SKILL.md` / `references/` instead.
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
skill_version: "0.1.
|
|
2
|
+
skill_version: "0.1.54"
|
|
3
3
|
alwaysApply: false
|
|
4
4
|
description: "Build Graphit HTML dashboards with KB-aware queries, entity wrapping, and cached data sources"
|
|
5
5
|
globs: []
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
> FROZEN - not maintained for Claude Code + Codex; may be stale. Source of truth is SKILL.md + references/.
|
|
9
|
+
|
|
8
10
|
# Graphit CLI
|
|
9
11
|
|
|
10
12
|
Build custom HTML dashboards from real data using the Graphit CLI.
|
|
@@ -84,13 +84,7 @@ Purpose before data. The first response should mirror the user's intent and ask
|
|
|
84
84
|
|
|
85
85
|
## Performance
|
|
86
86
|
|
|
87
|
-
`graphit.resolve()
|
|
88
|
-
|
|
89
|
-
- **Single refresh function.** All queries in ONE `Promise.all` inside one `refresh()` function. Never scatter `graphit.resolve()` across independent event handlers or timeouts - that turns one user action into multiple bursts.
|
|
90
|
-
- **Count your queries per interaction.** 6 charts = 6 requests per filter change = 20 changes/min budget. 12 charts = 10 changes/min. If you have 10+ charts with 3+ filters, consider debouncing filter changes (300ms) so rapid clicks don't each trigger a full refresh.
|
|
91
|
-
- **Reuse trend data for KPIs.** If you already fetch a weekly time series (`SELECT week, SUM(spend) ...`), derive the KPI total and sparkline from that result set in JS instead of running a separate aggregate query. One query serves both the chart and the KPI card.
|
|
92
|
-
- **Avoid redundant refreshes.** If a filter only affects some charts, split into targeted refresh functions (`refreshKPIs()`, `refreshCharts()`) so unchanged sections don't re-query.
|
|
93
|
-
- **No polling.** Never `setInterval(refresh, ...)`. Data sources update on their own schedule - a dashboard that polls wastes the entire rate budget.
|
|
87
|
+
Live data goes through `graphit.resolve()`, which is rate-limited per user per dashboard. Budget your queries per interaction - batch them, reuse trend data for KPIs, split targeted refreshes, and never poll. The full rate-limit budget and the efficient-refresh patterns live in `runtime.md`.
|
|
94
88
|
|
|
95
89
|
## Pre-Build Checklist
|
|
96
90
|
|
|
@@ -1,7 +1,18 @@
|
|
|
1
|
-
# Data Sources: Building for Speed
|
|
1
|
+
# Data Sources: Routing and Building for Speed
|
|
2
2
|
|
|
3
3
|
A data source's shape - set by its source SQL at creation - decides how fast every dashboard built on it will be, and whether filter changes feel instant. Get this right when you create the source; it can't be fixed later in the dashboard SQL.
|
|
4
4
|
|
|
5
|
+
## Routing: which source to query
|
|
6
|
+
|
|
7
|
+
Always prefer a cached data source over the live warehouse. Check what exists with `graphit ds list` before writing any query.
|
|
8
|
+
|
|
9
|
+
| Situation | Command | Speed |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| The table has a cached data source | `graphit query "SQL" --ds <id>` | roughly 100ms, DuckDB |
|
|
12
|
+
| No data source covers the table | `graphit query "SQL" --warehouse --connection <id>` | roughly 10s, Snowflake |
|
|
13
|
+
|
|
14
|
+
If no data source covers the table the user needs, propose creating one for future speed rather than defaulting to repeated warehouse queries. Dialect differs by route (DuckDB for `--ds`, Snowflake for `--warehouse`); see `sql-reference.md`.
|
|
15
|
+
|
|
5
16
|
## Why source shape decides dashboard speed
|
|
6
17
|
|
|
7
18
|
When you change a dashboard filter (e.g. switch country), Graphit answers from a cached, pre-aggregated result instead of re-scanning the whole source - but only when the source is small and already aggregated to the grain you query. Build the source as one row per the grain you'll chart (e.g. month x country x channel) with only the columns you use, and filter changes return in milliseconds. Pull in raw ungrouped rows, hundreds of columns, or thousands-of-values dimensions and the result is too big to cache - every filter change re-scans the entire source and is slow.
|
|
@@ -74,3 +85,21 @@ The CLI fires all refreshes in parallel and polls until every source is ready. L
|
|
|
74
85
|
## Deleting data sources
|
|
75
86
|
|
|
76
87
|
There is no `graphit ds delete` command. Deleting a data source cascades to GCS storage and the KB table, which removes all metrics, dimensions, and rules defined on it. Direct the user to delete from the platform UI (Sources Hub) where the confirmation flow shows what will be affected.
|
|
88
|
+
|
|
89
|
+
## Presenting data source results
|
|
90
|
+
|
|
91
|
+
The user cannot see the raw CLI output - you are the rendering layer. After `graphit ds list`, present a markdown table and end with a recommendation of which source to use (or note that none covers the needed table):
|
|
92
|
+
|
|
93
|
+
~~~
|
|
94
|
+
**3 data sources:**
|
|
95
|
+
|
|
96
|
+
| Name | ID | Rows | Status | Governed |
|
|
97
|
+
|---|---|---:|---|---|
|
|
98
|
+
| **MARKETING_UA_DS** | ds_abc123 | 1,247,832 | active | yes |
|
|
99
|
+
| **PLAYER_QUALITY** | ds_def456 | 892,104 | active | no |
|
|
100
|
+
| **REVENUE_EVENTS** | ds_ghi789 | 3,412,006 | stale | yes |
|
|
101
|
+
|
|
102
|
+
Using **MARKETING_UA_DS** (ds_abc123), which covers spend, installs, and ROAS columns.
|
|
103
|
+
~~~
|
|
104
|
+
|
|
105
|
+
Bold every data source name. If a source is stale, say so and offer to refresh it before querying.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Advanced Filter Controls - Dependent Dropdowns and Date Presets
|
|
2
|
+
|
|
3
|
+
Load only when you need one of two optional controls on top of the core filters: a dependent dropdown ("only relevant values" that narrows as an upstream filter changes) or a date-preset picker. The base filter, param, bind, wiring, `:name` binding, and saved-view mechanics live in `filters.md`. Both controls below are headless logic - you own all the markup.
|
|
4
|
+
|
|
5
|
+
## graphit.cascade(el, options) - Only Relevant Values
|
|
6
|
+
|
|
7
|
+
Dependent dropdowns: fetch a column's DISTINCT values constrained by other filters, and refetch when they change. For example, pick an org and the user list shows only that org's users. Logic only - you build the checkboxes or list in `render`.
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
const org = graphit.filter('org', { label: 'Org' })
|
|
11
|
+
const user = graphit.filter('user', { label: 'User', default: [] }) // multi-select
|
|
12
|
+
|
|
13
|
+
graphit.cascade('#user-list', {
|
|
14
|
+
column: 'USER_NAME', // distinct values of this column
|
|
15
|
+
source: 'users_table', // table name (or a subquery)
|
|
16
|
+
dataSourceId: 'ds_abc',
|
|
17
|
+
filters: () => ({ ORG: org.get() }), // upstream constraints; empty values skipped
|
|
18
|
+
deps: ['org'], // refetch when org changes
|
|
19
|
+
selection: user, // optional: prune selected users no longer in this org
|
|
20
|
+
render: (values, el, ctx) => {
|
|
21
|
+
// ctx = { loading, empty, error, hasUpstream } - build any markup you like
|
|
22
|
+
el.innerHTML = values.map(v => `<label><input type="checkbox" value="${v}"> ${v}</label>`).join('')
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- `filters()` returns `{ COLUMN: value }`. A scalar makes `COLUMN = :p`; an array makes `COLUMN IN :p`. Empty values (`null`, `''`, `[]`) are skipped (no constraint), so an empty upstream means "no filter", not "match nothing".
|
|
28
|
+
- `selection` (a filter handle) is auto-pruned to the surviving values when an upstream changes.
|
|
29
|
+
- Returns `{ destroy() }`. Keep the result set small (default `LIMIT 1001`); these parameterized queries skip the result cache, so they hit DuckDB directly.
|
|
30
|
+
- Faster for low-cardinality cascades: add `preload: true` to fetch the full distinct cross-product ONCE (cacheable, no params) and filter in-memory on every change - instant, zero per-change round-trips. Best when the column-by-upstream combinations are small (cap = `limit`, default 1001 tuples); above the cap it auto-falls-back to per-change server queries.
|
|
31
|
+
|
|
32
|
+
## graphit.dateRange(id, options) - Date Presets
|
|
33
|
+
|
|
34
|
+
A headless date filter with the standard presets built in (logic only - you render the buttons or inputs). `default` is a preset id or `{ start, end }`.
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
const dr = graphit.dateRange('date_range', { label: 'Date Range', default: 'last_30_days' })
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Handle:
|
|
41
|
+
- `dr.get()` returns `{ preset, start, end }` (ISO `YYYY-MM-DD`)
|
|
42
|
+
- `dr.set(presetId)` (e.g. `dr.set('this_month')`); `dr.setRange(start, end)` for a custom range
|
|
43
|
+
- `dr.start` / `dr.end` / `dr.preset` convenience getters; `dr.deps` (pass as `deps: dr.deps`); `dr.subscribe(cb)` (cb gets `{ preset, start, end }`)
|
|
44
|
+
|
|
45
|
+
A `dateRange` registration persists to saved views exactly like a `filter`/`param`, and its `subscribe` callback restores the picker's visual state on view apply or reload (same rule as the core controls in `filters.md`).
|
|
46
|
+
|
|
47
|
+
Relative presets auto-recompute on reload (a saved "last_7_days" always means the last 7 days from today). The 11 preset ids - also available via `graphit.datePresets` (`[{id,label}]`) and `graphit.datePreset(id)` (`{start,end}`): today, yesterday, last_7_days, last_30_days, this_month, last_month, this_quarter, last_quarter, ytd, last_90_days, last_12_months.
|
|
48
|
+
|
|
49
|
+
Bind a chart to the range with two scalar params and a `BETWEEN`:
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
graphit.bind('#rev', {
|
|
53
|
+
sql: 'SELECT day, SUM(rev) AS rev FROM orders WHERE day BETWEEN :start_date AND :end_date GROUP BY 1',
|
|
54
|
+
dataSourceId: 'ds_abc',
|
|
55
|
+
params: () => ({ start_date: dr.start, end_date: dr.end }),
|
|
56
|
+
deps: dr.deps,
|
|
57
|
+
render: (r, el) => graphit.chart(el, { type: 'area', data: r.data, x: 'day', y: 'rev' }),
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Name the placeholders `:start_date` / `:end_date`, never `:from` / `:to` - reserved-word placeholders fail SQL validation (see the `:name` rules in `filters.md`).
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
# Headless Filters, Parameters, and Saved Views
|
|
2
2
|
|
|
3
|
+
Load when the dashboard needs interactivity: a control the user changes that re-resolves a chart, plus saved views. A static dashboard skips this file. Dependent dropdowns and date presets live in `filters-advanced.md`.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
Core Concept | `graphit.filter()` / `graphit.param()` | Wiring a Control | `graphit.bind()` | Safe Parameter Binding (`:name`) | Saved Views | Complete Example
|
|
8
|
+
|
|
3
9
|
## Core Concept
|
|
4
10
|
|
|
5
11
|
`graphit.filter()` and `graphit.param()` are headless JS registrations. They create NO DOM elements. You build 100% of the filter control's markup (any HTML, SVG, or CSS). The registration manages state so values can be snapshotted into saved views and survive page reloads.
|
|
6
12
|
|
|
7
|
-
**Logic vs style.** `filter`, `param`, `bind
|
|
13
|
+
**Logic vs style.** `filter`, `param`, `bind` are headless logic - zero imposed styling, you own the markup. `chart`, `table`, `kpi`, `presentation`, `dropdown` render styled output you can use or hand-roll past.
|
|
14
|
+
|
|
15
|
+
These four pieces always work together: register a control, register the binding that reads it, wire your markup to the handle, and let saved views snapshot it. A filter with no `bind` does nothing - plan to include all four when you add interactivity.
|
|
8
16
|
|
|
9
17
|
## graphit.filter(id, options)
|
|
10
18
|
|
|
@@ -25,56 +33,15 @@ Handle API:
|
|
|
25
33
|
|
|
26
34
|
## graphit.param(id, options)
|
|
27
35
|
|
|
28
|
-
Same API as filter but semantically a parameter (not tied to a KB field). Use for user-controlled inputs like
|
|
36
|
+
Same API as filter but semantically a parameter (not tied to a KB field). Use for user-controlled inputs like threshold sliders or toggle switches.
|
|
29
37
|
|
|
30
38
|
```js
|
|
31
39
|
const topN = graphit.param('top_n', { label: 'Top N', default: 10, options: [5, 10, 20, 50] })
|
|
32
40
|
```
|
|
33
41
|
|
|
34
|
-
## graphit.dateRange(id, options)
|
|
35
|
-
|
|
36
|
-
A headless date filter with the standard presets built in (logic only - you render the buttons/inputs). `default` is a preset id or `{ start, end }`.
|
|
37
|
-
|
|
38
|
-
```js
|
|
39
|
-
const dr = graphit.dateRange('date_range', { label: 'Date Range', default: 'last_30_days' })
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Handle:
|
|
43
|
-
- `dr.get()` -> `{ preset, start, end }` (ISO `YYYY-MM-DD`)
|
|
44
|
-
- `dr.set(presetId)` (e.g. `dr.set('this_month')`); `dr.setRange(start, end)` for a custom range
|
|
45
|
-
- `dr.start` / `dr.end` / `dr.preset` convenience getters; `dr.deps` (pass as `deps: dr.deps`); `dr.subscribe(cb)` (cb gets `{ preset, start, end }`)
|
|
46
|
-
|
|
47
|
-
Relative presets auto-recompute on reload (a saved "last_7_days" always means the last 7 days from today). The 11 preset ids - also via `graphit.datePresets` (`[{id,label}]`) and `graphit.datePreset(id)` (`{start,end}`): today, yesterday, last_7_days, last_30_days, this_month, last_month, this_quarter, last_quarter, ytd, last_90_days, last_12_months.
|
|
48
|
-
|
|
49
|
-
Bind a chart to the range with two scalar params and a `BETWEEN`:
|
|
50
|
-
|
|
51
|
-
```js
|
|
52
|
-
graphit.bind('#rev', {
|
|
53
|
-
sql: 'SELECT day, SUM(rev) AS rev FROM orders WHERE day BETWEEN :start_date AND :end_date GROUP BY 1',
|
|
54
|
-
dataSourceId: 'ds_abc',
|
|
55
|
-
params: () => ({ start_date: dr.start, end_date: dr.end }),
|
|
56
|
-
deps: dr.deps,
|
|
57
|
-
render: (r, el) => graphit.chart(el, { type: 'area', data: r.data, x: 'day', y: 'rev' }),
|
|
58
|
-
})
|
|
59
|
-
```
|
|
60
|
-
|
|
61
42
|
## Wiring a Control
|
|
62
43
|
|
|
63
|
-
The registration renders nothing
|
|
64
|
-
|
|
65
|
-
```html
|
|
66
|
-
<select id="country-picker">
|
|
67
|
-
<option value="US">US</option>
|
|
68
|
-
<option value="IL">Israel</option>
|
|
69
|
-
</select>
|
|
70
|
-
<script>
|
|
71
|
-
const country = graphit.filter('country', { label: 'Country', default: 'US' });
|
|
72
|
-
const picker = document.getElementById('country-picker');
|
|
73
|
-
picker.value = country.get();
|
|
74
|
-
picker.onchange = () => country.set(picker.value);
|
|
75
|
-
country.subscribe(v => { picker.value = v; }); // restores on view apply or page reload
|
|
76
|
-
</script>
|
|
77
|
-
```
|
|
44
|
+
The registration renders nothing, so connect your own markup to the handle in three steps: set the element's initial value from `handle.get()`, call `handle.set(newValue)` in the change event, and pass `handle.subscribe(v => updateElement(v))` so the control restores its visual state on saved-view apply or page reload. The Complete Example below shows this end to end for a `<select>`.
|
|
78
45
|
|
|
79
46
|
## graphit.bind(el, options) - Reactive Data Binding
|
|
80
47
|
|
|
@@ -96,36 +63,9 @@ graphit.bind(document.getElementById('revenue-chart'), {
|
|
|
96
63
|
- Multi-key changes (e.g. a saved view applying 3 filters) debounce into one re-resolve per element
|
|
97
64
|
- `render` is your code - call `graphit.chart`, `graphit.table`, or hand-roll SVG/CSS
|
|
98
65
|
|
|
99
|
-
## graphit.cascade(el, options) - Only Relevant Values
|
|
100
|
-
|
|
101
|
-
Dependent dropdowns: fetch a column's DISTINCT values constrained by other filters, and refetch when they change (e.g. pick an org, the user list shows only that org's users). Logic only - you build the checkboxes/list in `render`.
|
|
102
|
-
|
|
103
|
-
```js
|
|
104
|
-
const org = graphit.filter('org', { label: 'Org' })
|
|
105
|
-
const user = graphit.filter('user', { label: 'User', default: [] }) // multi-select
|
|
106
|
-
|
|
107
|
-
graphit.cascade('#user-list', {
|
|
108
|
-
column: 'USER_NAME', // distinct values of this column
|
|
109
|
-
source: 'users_table', // table name (or a subquery)
|
|
110
|
-
dataSourceId: 'ds_abc',
|
|
111
|
-
filters: () => ({ ORG: org.get() }), // upstream constraints; empty values skipped
|
|
112
|
-
deps: ['org'], // refetch when org changes
|
|
113
|
-
selection: user, // optional: prune selected users no longer in this org
|
|
114
|
-
render: (values, el, ctx) => {
|
|
115
|
-
// ctx = { loading, empty, error, hasUpstream } - build any markup you like
|
|
116
|
-
el.innerHTML = values.map(v => `<label><input type="checkbox" value="${v}"> ${v}</label>`).join('')
|
|
117
|
-
},
|
|
118
|
-
})
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
- `filters()` returns `{ COLUMN: value }`. A scalar makes `COLUMN = :p`; an array makes `COLUMN IN :p`. Empty values (`null`, `''`, `[]`) are skipped (no constraint), so an empty upstream means "no filter", not "match nothing".
|
|
122
|
-
- `selection` (a filter handle) is auto-pruned to the surviving values when an upstream changes.
|
|
123
|
-
- Returns `{ destroy() }`. Keep the result set small (default `LIMIT 1001`); these parameterized queries skip the result cache, so they hit DuckDB directly.
|
|
124
|
-
- **Faster for low-cardinality cascades:** add `preload: true` to fetch the full distinct cross-product ONCE (cacheable, no params) and filter in-memory on every change - instant, zero per-change round-trips. Best when the column x upstream combinations are small (cap = `limit`, default 1001 tuples); above the cap it auto-falls-back to per-change server queries.
|
|
125
|
-
|
|
126
66
|
## Safe Parameter Binding (`:name` syntax)
|
|
127
67
|
|
|
128
|
-
Use `:name` placeholders in SQL for safe server-side parameter binding.
|
|
68
|
+
Use `:name` placeholders in SQL for safe server-side parameter binding. NEVER string-concatenate user values into SQL.
|
|
129
69
|
|
|
130
70
|
| Shape | SQL | Params |
|
|
131
71
|
|-------|-----|--------|
|
|
@@ -133,7 +73,7 @@ Use `:name` placeholders in SQL for safe server-side parameter binding. Never st
|
|
|
133
73
|
| Multi-select | `WHERE country IN :countries` | `{countries: ['US', 'IL']}` (expands to safe `IN ($0, $1)`) |
|
|
134
74
|
| Date range | `WHERE date BETWEEN :start_date AND :end_date` | `{start_date: '2026-01-01', end_date: '2026-06-01'}` |
|
|
135
75
|
|
|
136
|
-
|
|
76
|
+
Do NOT name a param after a SQL keyword (`from`, `to`, `select`, `order`, `group`, and similar). The SQL template is parsed before values bind, so a reserved-word placeholder like `:from` fails with "SQL validation failed". Use names like `:start_date`, `:end_date`.
|
|
137
77
|
|
|
138
78
|
Array length capped at 200 elements, max 50 param keys per resolve call.
|
|
139
79
|
|
|
@@ -141,26 +81,20 @@ Array length capped at 200 elements, max 50 param keys per resolve call.
|
|
|
141
81
|
|
|
142
82
|
Users can save the current filter/parameter state as a named view and restore it later. The platform snapshots all registered `graphit.filter` and `graphit.param` values automatically. Views survive page reloads (state is baked into the iframe on every render). A default view auto-applies on dashboard open with no flash.
|
|
143
83
|
|
|
144
|
-
The subscribe callback on each handle restores the control's visual state when a view is applied
|
|
84
|
+
The subscribe callback on each handle restores the control's visual state when a view is applied. This is why every control MUST include a `subscribe(v => ...)` that updates its appearance. A control registered without `graphit.filter`/`param` (a hand-rolled `<select>` on its own) will NOT persist to a view.
|
|
145
85
|
|
|
146
86
|
## Complete Example
|
|
147
87
|
|
|
88
|
+
One control, wired to one reactive chart. The `<select>` is your own markup; `bind()` re-resolves the chart whenever the filter changes.
|
|
89
|
+
|
|
148
90
|
```html
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
<
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
</select>
|
|
157
|
-
</div>
|
|
158
|
-
|
|
159
|
-
<div id="revenue-by-region" data-graphit-id="revenue-by-region"
|
|
160
|
-
data-graphit-label="Revenue by Region"
|
|
161
|
-
data-graphit-sql="SELECT month, SUM(revenue) AS revenue FROM sales WHERE region = :region GROUP BY 1 ORDER BY 1"
|
|
162
|
-
data-graphit-ds="ds_sales">
|
|
163
|
-
</div>
|
|
91
|
+
<label>Region</label>
|
|
92
|
+
<select id="region-select">
|
|
93
|
+
<option value="ALL">All Regions</option>
|
|
94
|
+
<option value="NA">North America</option>
|
|
95
|
+
<option value="EU">Europe</option>
|
|
96
|
+
</select>
|
|
97
|
+
<div id="revenue-by-region"></div>
|
|
164
98
|
|
|
165
99
|
<script>
|
|
166
100
|
const region = graphit.filter('region', { label: 'Region', field: 'REGION', default: 'ALL' });
|
|
@@ -171,11 +105,9 @@ The subscribe callback on each handle restores the control's visual state when a
|
|
|
171
105
|
region.subscribe(v => { sel.value = v; });
|
|
172
106
|
|
|
173
107
|
graphit.bind(document.getElementById('revenue-by-region'), {
|
|
174
|
-
sql:
|
|
175
|
-
${region.get() !== 'ALL' ? 'WHERE region = :region' : ''}
|
|
176
|
-
GROUP BY 1 ORDER BY 1`,
|
|
108
|
+
sql: 'SELECT month, SUM(revenue) AS revenue FROM sales WHERE (:region = \'ALL\' OR region = :region) GROUP BY 1 ORDER BY 1',
|
|
177
109
|
dataSourceId: 'ds_sales',
|
|
178
|
-
params: () =>
|
|
110
|
+
params: () => ({ region: region.get() }),
|
|
179
111
|
deps: ['region'],
|
|
180
112
|
render: (result, el) => {
|
|
181
113
|
graphit.chart(el, { type: 'area', data: result.data, x: 'month', y: 'revenue', valueFormat: 'currency' });
|
|
@@ -183,3 +115,5 @@ The subscribe callback on each handle restores the control's visual state when a
|
|
|
183
115
|
});
|
|
184
116
|
</script>
|
|
185
117
|
```
|
|
118
|
+
|
|
119
|
+
For dependent dropdowns (an "only relevant values" list that narrows as an upstream filter changes) and date-preset pickers, see `filters-advanced.md`.
|
|
@@ -1,113 +1,96 @@
|
|
|
1
1
|
# Query Governance
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Load this when writing or validating a governed query, working trust tiers, or working the ad-hoc frontier (deciding whether a raw measure run is allowed on a governed source).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Governance is enforced server-side in the QueryGateway, the same across every channel. You CANNOT weaken it from the CLI; you can only write queries that pass it or, where the mode allows, run a raw measure with explicit approval.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Reference syntax
|
|
8
8
|
|
|
9
|
-
|
|
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
|
|
9
|
+
Write governed queries with KB references, not inline formulas. The server compiles each to its KB expression, injects rule constraints, stamps the trust tier, and caches the result. The syntax is identical in `graphit query` and `graphit.resolve()`.
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
| Syntax | Expands to | Example |
|
|
12
|
+
|--------|-----------|---------|
|
|
13
|
+
| `{{metric:NAME}}` | Metric calculation (aggregation) | `{{metric:CPI}}` |
|
|
14
|
+
| `{{metric:NAME(K=V)}}` | Parameterized metric | `{{metric:ARPU(DAY=7)}}` |
|
|
15
|
+
| `{{metric_raw:NAME}}` | Raw expression, no outer aggregate | `{{metric_raw:REVENUE}}` |
|
|
16
|
+
| `{{dim:NAME}}` | Dimension expression | `{{dim:INSTALL_MONTH}}` |
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
SELECT {{
|
|
18
|
+
```bash
|
|
19
|
+
graphit query "SELECT {{dim:INSTALL_MONTH}}, {{metric:CPI}} AS cpi FROM MARKETING_UA_DS GROUP BY 1" --ds ds_abc123 --verbose
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
`--verbose` prints the expanded SQL and trust tier, so you can confirm the reference resolved before presenting the result.
|
|
23
|
+
|
|
24
|
+
## Parameterized metrics
|
|
25
|
+
|
|
26
|
+
Some metrics (for example ARPU, ROAS, RETENTION) carry required parameters and cannot resolve without a value. Run `graphit kb list metric` and read the `params` column for the names a metric requires, then supply them inline as `{{metric:ARPU(DAY=7)}}`. Pre-baked variants such as `ARPU_D7` or `ROAS_D30` have the value fixed and need none. Omitting a required parameter returns a clear error naming the exact syntax, so read `params` first.
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
## Trust tiers
|
|
25
29
|
|
|
26
|
-
|
|
30
|
+
Every result is stamped with a tier the platform shows the user as a badge on dashboard graphs and canvas entities - your honest signal of how trustworthy the number is.
|
|
27
31
|
|
|
28
32
|
| Tier | Meaning | Badge |
|
|
29
33
|
|------|---------|-------|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
34
|
+
| `governed` | Query used `{{metric:X}}` / `{{dim:X}}` references | Teal dot |
|
|
35
|
+
| `verified` | Raw SQL whose expressions match KB definitions | Amber dot |
|
|
36
|
+
| `ad_hoc` | Inline formulas with no KB match | Gray dot |
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
Prefer the governed tier. The server may upgrade matching raw SQL to verified, but reach for references first so the result is governed by intent.
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
|------|----------|
|
|
38
|
-
| `observe` | All queries pass, tiers are tracked |
|
|
39
|
-
| `warn` | Ad-hoc **measure** queries on governed DSes ask for approval; exploration warns |
|
|
40
|
-
| `strict` | All ad-hoc queries on governed DSes are blocked |
|
|
40
|
+
## The ad-hoc measure gate
|
|
41
41
|
|
|
42
|
-
|
|
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
|
-
```
|
|
42
|
+
This is the hard frontier, enforced server-side. The rules below are what the QueryGateway does, not a suggestion you can soften.
|
|
47
43
|
|
|
48
|
-
|
|
44
|
+
An **ad-hoc measure** computes a business measure (an aggregate or `GROUP BY` that produces a metric) on a governed data source WITHOUT `{{metric:NAME}}` references, so it lands at the `ad_hoc` tier. Plain exploration is not a measure: `SELECT *`, raw column selects, `COUNT(*)` row-count peeks, `DISTINCT` value lists.
|
|
49
45
|
|
|
50
|
-
|
|
46
|
+
- **warn.** The ad-hoc measure is rejected and asks for approval. First rewrite it with `{{metric:NAME}}` / `{{dim:NAME}}` references (search the KB for a match). Only if the user genuinely needs the raw run, ask them, then re-run the exact command with `--approve-adhoc`. Plain exploration is **exempt**: it runs free, with only a non-blocking warning.
|
|
47
|
+
- **strict.** CRITICAL: every `ad_hoc`-tier query is a HARD BLOCK, including plain exploration that does not match the KB. Rewrite with references or stop. `--approve-adhoc` NEVER bypasses strict and does not weaken it; do not offer it in strict mode.
|
|
48
|
+
- **observe.** All queries run; tiers are tracked, nothing is gated.
|
|
51
49
|
|
|
52
|
-
|
|
53
|
-
- **strict mode:** all ad-hoc queries are hard-blocked. Rewrite with references or stop; `--approve-adhoc` does not bypass strict.
|
|
50
|
+
When the gate fires, do not narrate around it or pretend the query ran. Report truthfully: it was blocked or needs approval, name the governed rewrite, and let the user decide.
|
|
54
51
|
|
|
55
|
-
## Enforceable
|
|
52
|
+
## Enforceable rules and overrides
|
|
56
53
|
|
|
57
|
-
Rules with typed constraints are enforced automatically
|
|
54
|
+
Rules with typed constraints are enforced automatically, rewriting the SQL before it runs:
|
|
58
55
|
|
|
59
56
|
| Type | What it does |
|
|
60
57
|
|------|-------------|
|
|
61
58
|
| `required_where` | Injects a WHERE predicate |
|
|
62
59
|
| `forbidden_column` | NULLifies a column in SELECT |
|
|
63
|
-
| `value_restriction` | Restricts column to allowed values |
|
|
64
|
-
| `required_filter` | Validates column appears in WHERE |
|
|
65
|
-
| `required_aggregation` | Validates GROUP BY includes column |
|
|
66
|
-
|
|
67
|
-
User-context variables (`${user.team_id}`, `${user.email}`) are resolved server-side for row-level security.
|
|
60
|
+
| `value_restriction` | Restricts a column to allowed values |
|
|
61
|
+
| `required_filter` | Validates a column appears in WHERE |
|
|
62
|
+
| `required_aggregation` | Validates GROUP BY includes a column |
|
|
68
63
|
|
|
69
|
-
|
|
64
|
+
User-context variables (`${user.team_id}`, `${user.email}`) resolve server-side for row-level security. Override a rule only when the user explicitly asks and the rule's `override_policy` (anyone / analyst_only / admin_only / never) and the user's role allow it; a `never` policy can never be overridden, and every override is logged. Pass several names to override more than one.
|
|
70
65
|
|
|
71
66
|
```bash
|
|
72
|
-
# Override a specific rule
|
|
73
67
|
graphit query "SELECT * FROM events" --ds ds_123 --override-rules EXCLUDE_RETARGETING
|
|
74
|
-
|
|
75
|
-
# Multiple overrides
|
|
76
|
-
graphit query "SELECT * FROM events" --ds ds_123 --override-rules RULE1 RULE2
|
|
77
68
|
```
|
|
78
69
|
|
|
79
|
-
|
|
70
|
+
## Per-DS settings and governance mode
|
|
80
71
|
|
|
81
|
-
|
|
72
|
+
Governed mode and the row cap are set per data source; enabling governed mode activates the ad-hoc gate for that DS. The governance mode is org-wide and admin-only, set with a `--mode` flag, not a positional argument (values `observe`, `warn`, `strict`). Run `graphit ds update --help` and `graphit governance set --help` for exact flag spelling.
|
|
82
73
|
|
|
83
74
|
```bash
|
|
84
|
-
graphit
|
|
85
|
-
graphit ds update <id> --governed-mode off # Disable
|
|
86
|
-
graphit ds update <id> --max-rows 10000 # Set row cap
|
|
75
|
+
graphit governance set --mode warn
|
|
87
76
|
```
|
|
88
77
|
|
|
89
|
-
## Presenting
|
|
90
|
-
|
|
91
|
-
**After `graphit governance status`:**
|
|
92
|
-
|
|
93
|
-
~~~
|
|
94
|
-
**Governance mode:** strict
|
|
78
|
+
## Presenting governance results
|
|
95
79
|
|
|
96
|
-
|
|
80
|
+
The user CANNOT see raw CLI output. Render every result as markdown.
|
|
97
81
|
|
|
98
|
-
|
|
99
|
-
|---|---:|---:|
|
|
100
|
-
| Governed | 847 | 72% |
|
|
101
|
-
| Ad-hoc | 328 | 28% |
|
|
102
|
-
| Blocked | 12 | 1% |
|
|
82
|
+
**After a governed query**, append a provenance footer so the trust signal travels with the number: tier, governed DS, KB references used, rules enforced by name, row cap if applied. For an ad-hoc result, state the tier honestly and offer the governed `{{metric:NAME}}` / `{{dim:NAME}}` rewrite.
|
|
103
83
|
|
|
104
|
-
5 active rules across 3 governed data sources.
|
|
105
84
|
~~~
|
|
85
|
+
**Trust tier:** governed - governed DS, 2 KB refs, 1 rule enforced (**EXCLUDE_INTERNAL**), max rows 10000
|
|
86
|
+
~~~
|
|
87
|
+
|
|
88
|
+
**After `graphit governance status`**, show the mode plus the 7-day conformance counts (governed / verified / ad-hoc / total) as a small markdown table headed by `**Governance mode:** <mode>`.
|
|
106
89
|
|
|
107
|
-
**
|
|
90
|
+
**When a gate or rule blocks a query**, explain it and the path forward, no raw dump:
|
|
108
91
|
|
|
109
92
|
~~~
|
|
110
|
-
**
|
|
93
|
+
**Blocked by governance.**
|
|
111
94
|
|
|
112
|
-
Rule **EXCLUDE_ORGANIC**
|
|
95
|
+
Rule **EXCLUDE_ORGANIC** has override policy `never` and cannot be overridden. Ask your admin to change the policy, or rewrite the query to include the required filter.
|
|
113
96
|
~~~
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Graphit Visual Style
|
|
2
2
|
|
|
3
|
-
Consult when
|
|
3
|
+
Consult when authoring the dashboard HTML. This is the design system: principles, the Graphit aesthetic, typography, the color-token system, and layout CSS. Data wiring (resolve, entity wrapping, the first-paint loading overlay) lives in `runtime.md`; per-chart implementations live in `chart-patterns.md`.
|
|
4
4
|
|
|
5
5
|
## Design Principles
|
|
6
6
|
|
|
@@ -84,12 +84,6 @@ Use the system font stack everywhere: `-apple-system, BlinkMacSystemFont, 'Segoe
|
|
|
84
84
|
box-shadow:0 1px 3px rgba(0,0,0,0.08); }
|
|
85
85
|
.card h3 { font-size:12px; font-weight:600; color:var(--graphit-fg-subtle); text-transform:uppercase;
|
|
86
86
|
letter-spacing:0.05em; margin-bottom:16px; }
|
|
87
|
-
@keyframes gh-spin{to{transform:rotate(360deg)}}
|
|
88
|
-
.gh-loading { position:relative; min-height:120px; }
|
|
89
|
-
.gh-loading-overlay { position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
|
|
90
|
-
z-index:9998; backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px);
|
|
91
|
-
background:color-mix(in srgb,var(--graphit-surface-raised,#fff) 50%,transparent); border-radius:inherit; }
|
|
92
|
-
.gh-loading-spin { animation:gh-spin .7s linear infinite; }
|
|
93
87
|
@media(max-width:900px) {
|
|
94
88
|
.kpi-grid { grid-template-columns:repeat(2,1fr); }
|
|
95
89
|
.charts-grid { grid-template-columns:1fr; }
|
|
@@ -97,6 +91,8 @@ Use the system font stack everywhere: `-apple-system, BlinkMacSystemFont, 'Segoe
|
|
|
97
91
|
</style>
|
|
98
92
|
```
|
|
99
93
|
|
|
94
|
+
Also add the first-paint loading-overlay CSS (the `gh-loading` family) to the page `<style>` and wrap every resolve target with it. That overlay is the SDK contract - the canonical CSS and rules are in `runtime.md`.
|
|
95
|
+
|
|
100
96
|
### Dashboard Composition (top to bottom)
|
|
101
97
|
1. **Title row** - dashboard name, optional subtitle with date range
|
|
102
98
|
2. **KPI row** - 3-4 cards in a grid. Each: big number, label, delta badge
|
|
@@ -129,16 +125,8 @@ Use the system font stack everywhere: `-apple-system, BlinkMacSystemFont, 'Segoe
|
|
|
129
125
|
</style>
|
|
130
126
|
```
|
|
131
127
|
|
|
132
|
-
### Loading
|
|
133
|
-
|
|
134
|
-
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:
|
|
135
|
-
|
|
136
|
-
```html
|
|
137
|
-
<div id="spend-chart" class="gh-loading">
|
|
138
|
-
<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>
|
|
139
|
-
</div>
|
|
140
|
-
```
|
|
128
|
+
### Loading state
|
|
141
129
|
|
|
142
|
-
|
|
130
|
+
The first-paint loading overlay (the `gh-loading` overlay baked into every resolve target) is part of the data-wiring contract with the SDK. Its canonical CSS, the markup, and the rule about which elements get it live in `runtime.md`.
|
|
143
131
|
|
|
144
|
-
For
|
|
132
|
+
For per-chart implementations (bar, line, donut, heatmap, funnel, sparkline, gauge, stacked bar, multi-series), see `chart-patterns.md`.
|