@graphit/cli 0.2.236 → 0.2.242

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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "Graphit CLI plugin for AI coding assistants",
10
- "version": "0.2.236"
10
+ "version": "0.2.242"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -16,9 +16,9 @@
16
16
  "source": {
17
17
  "source": "npm",
18
18
  "package": "@graphit/cli",
19
- "version": "0.2.236"
19
+ "version": "0.2.242"
20
20
  },
21
- "version": "0.2.236",
21
+ "version": "0.2.242",
22
22
  "category": "data-visualization",
23
23
  "tags": [
24
24
  "bi",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphit",
3
- "version": "0.2.236",
3
+ "version": "0.2.242",
4
4
  "description": "Build custom HTML dashboards from real data using the Graphit CLI. KB-aware queries, entity wrapping, cached data sources.",
5
5
  "author": {
6
6
  "name": "Graphit",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphit",
3
- "version": "0.2.236",
3
+ "version": "0.2.242",
4
4
  "description": "Build custom HTML dashboards from real data using the Graphit CLI. KB-aware queries, entity wrapping, cached data sources.",
5
5
  "author": {
6
6
  "name": "Graphit",
package/bin/graphit CHANGED
@@ -14,7 +14,7 @@ if [ -z "${GRAPHIT_PLUGIN_ROOT:-}" ]; then
14
14
  fi
15
15
 
16
16
  # graphit:floor (stamped by scripts/sync-plugin-version.mjs from cli/package.json)
17
- FLOOR_VERSION="0.2.236"
17
+ FLOOR_VERSION="0.2.242"
18
18
 
19
19
  PACKAGE_NAME="@graphit/cli"
20
20
  # Strict semver: anything else is rejected so a tampered cache cannot inject.
package/bin/graphit.ps1 CHANGED
@@ -7,7 +7,7 @@ if (-not $env:GRAPHIT_PLUGIN_ROOT) {
7
7
  }
8
8
 
9
9
  # graphit:floor (stamped by scripts/sync-plugin-version.mjs from cli/package.json)
10
- $FloorVersion = "0.2.236"
10
+ $FloorVersion = "0.2.242"
11
11
 
12
12
  $PackageName = "@graphit/cli"
13
13
  # Strict semver: anything else is rejected so a tampered cache cannot inject.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphit/cli",
3
- "version": "0.2.236",
3
+ "version": "0.2.242",
4
4
  "description": "Graphit CLI - Build custom dashboards from any AI coding assistant",
5
5
  "repository": {
6
6
  "type": "git",
@@ -2,7 +2,7 @@
2
2
  name: graphit
3
3
  description: >-
4
4
  Use Graphit for ANY question about the user's business or product data: metrics, KPIs, revenue, retention, spend, users, cohorts, funnels, trends, comparisons, "why did X change", "how are we doing on Y", analysis, reports, or dashboards. Activate even when the user does not say "Graphit" or name any tool: if someone wants to understand their numbers, this is the tool. Graphit answers through a governed semantic layer (computed the team's way, reusable and safe to share) and delivers the answer as a fast cached-data query or a hand-authored interactive HTML dashboard, and can create the metrics, dimensions, and rules an answer needs. Prefer Graphit over hand-rolled one-off analysis whenever the data is, or could be, the user's business data. Skip only for pure software tasks (code, logs, config, infra) or data with nothing to do with the user's business.
5
- skill_version: "0.2.236"
5
+ skill_version: "0.2.242"
6
6
  ---
7
7
 
8
8
  <!-- SIZE EXEMPTION (SKILL.md): standard hard limit 12,288 chars, exempted ceiling 29,696. Always-loaded: the collaboration/pace spine, hard constraints + scope gate, the loop, and the generated command table (COMMANDS markers, scripts/generate-commands-doc.mjs) - needed every turn, cannot defer to a reference. Marker sits after the frontmatter so the loader and sync-plugin-version.mjs parse it. Reviewed 2026-07-20. -->
@@ -148,13 +148,14 @@ Read the one that matches what you are doing now. Do not preload them. Exact com
148
148
  | designing and rendering the dashboard | dashboard-planning.md, chart-selection.md, chart-patterns.md, graphit-style.md, runtime.md, kpi.md, table.md |
149
149
  | adding interactivity (filters, parameters, saved views) | filters.md, filters-advanced.md |
150
150
  | building a slide deck | presentations.md |
151
+ | moving an existing dashboard's queries onto its entities, or explaining a legacy-query save warning | migration.md |
151
152
  | the CLI or plugin itself (health, permission errors, local working artifacts) | operations.md |
152
153
  | installing, updating, or repairing Graphit itself | install-update.md |
153
154
  | reporting a failure or a partial result | reporting.md |
154
155
 
155
156
  ## Commands
156
157
 
157
- Graphit is one CLI, but how you invoke it depends on your environment. On Claude Code the plugin provides a `graphit` wrapper, so `graphit <command>` runs the current CLI. On Codex, Cursor, a terminal, or CI there is no `graphit` wrapper - invoke the CLI explicitly with `npx -y @graphit/cli@0.2.236 <command>` (a stamped version, kept current by the build; pin an exact version for a reproducible run). The table below is generated from the CLI itself. For exact flags, run `graphit <command> --help` - never guess a flag.
158
+ Graphit is one CLI, but how you invoke it depends on your environment. On Claude Code the plugin provides a `graphit` wrapper, so `graphit <command>` runs the current CLI. On Codex, Cursor, a terminal, or CI there is no `graphit` wrapper - invoke the CLI explicitly with `npx -y @graphit/cli@0.2.242 <command>` (a stamped version, kept current by the build; pin an exact version for a reproducible run). The table below is generated from the CLI itself. For exact flags, run `graphit <command> --help` - never guess a flag.
158
159
 
159
160
  <!-- COMMANDS:START -->
160
161
 
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@graphit/cli",
3
- "version": "0.2.236",
3
+ "version": "0.2.242",
4
4
  "source": "cli/package.json"
5
5
  }
@@ -56,7 +56,7 @@ For a custom look, draw your own SVG through the same entry: `graphit.graph(el,
56
56
  Escaping is yours: data-derived text through `ctx.esc()`, author colors through `ctx.safeColor()` - the runtime does not auto-escape your marks. Opt out per concern with `responsive: false` (render once) or `themed: false` (no dark re-draw).
57
57
 
58
58
  ```js
59
- const r = await graphit.resolve({ sql, dataSourceId: "ORDERS_DS", target: "#chart" });
59
+ const r = await graphit.resolve({ target: "#chart" });
60
60
  graphit.graph("#chart", { type: "custom", draw: (ctx) => r.data.map(function (row, i) {
61
61
  var h = ctx.num(row.value) / 100 * ctx.height, x = (ctx.width / r.data.length) * i;
62
62
  return '<rect x="' + x + '" y="' + (ctx.height - h) + '" width="22" height="' + h +
@@ -0,0 +1,89 @@
1
+ # Migrating a dashboard to entity-owned queries
2
+
3
+ Load when the user asks to move an existing dashboard's queries onto its entities, or what a
4
+ `legacy_query_source` save warning means. Not for authoring new dashboards: new work is canonical
5
+ from the start (references/runtime.md).
6
+
7
+ ## What the old shape is
8
+
9
+ Older dashboards author the same query twice. The call carries it:
10
+
11
+ ```js
12
+ graphit.resolve({ sql: "SELECT ... WHERE d >= :start", dataSourceId: "MARKETING_UA_DS",
13
+ params: { start: pStart.get() }, sourceEntityId: "spend-trend" })
14
+ ```
15
+
16
+ and the entity carries a copy in `data-graphit-sql` / `data-graphit-ds`. Both still work, and they
17
+ drift: only the call executes, only the attribute is inspected.
18
+
19
+ Canonical is the same query written once, on the entity, the call naming which entity to run:
20
+
21
+ ```js
22
+ graphit.resolve({ sourceEntityId: "spend-trend", params: { start: pStart.get() } })
23
+ ```
24
+
25
+ ## Hard rules
26
+
27
+ **NEVER migrate a dashboard the user did not ask about.** This is opt-in. An unrelated edit never
28
+ rewires where a query lives, and "while I was in there" is not a reason.
29
+
30
+ **NEVER save a partial or unverified migration.** Half-moved is worse than not moved. If you cannot
31
+ finish and verify a query, put it back the way you found it.
32
+
33
+ **MUST leave the dashboard untouched when you cannot prove equivalence.** Stop, change nothing, and
34
+ name the exact branch you could not compare. That is a successful outcome, not a failure.
35
+
36
+ ## Procedure
37
+
38
+ 1. **Read everything first.** Get the full HTML and all its wiring before changing one call.
39
+ Migrating call by call misses shared queries and conditional branches.
40
+ 2. **Find the owner of each query.** One source entity owns it; any other entity rendered from the
41
+ same result is a target. A query feeding three graphs still has exactly one owner.
42
+ 3. **Lift the complete template onto the owner.** The whole statement in `data-graphit-sql`, the data
43
+ source in `data-graphit-ds`. Complete means executable and parameterized:
44
+ - Keep every `:named` placeholder. Do not bake current filter values in.
45
+ - Keep `{{metric:NAME}}` / `{{dim:NAME}}` references as they are.
46
+ - No ellipsis, no abbreviation, real table names, full WITH clause.
47
+ 4. **Preserve the rest exactly.** `params`, `deps`, the `render` callback, any branch that picks
48
+ different SQL, and `sourceEntityId` / `targetEntityIds` attribution all stay as they were.
49
+ Migration moves where the query lives; it does not rewrite the query, wiring, or layout.
50
+ 5. **Build the filter-state matrix.** The default state, plus every materially distinct non-default
51
+ or conditional branch found in step 1. A branch that selects different SQL is its own state.
52
+ 6. **Prove equivalence per state, on four signals.** For each state, compare before and after:
53
+
54
+ | Signal | Where it comes from |
55
+ |---|---|
56
+ | Effective SQL | the details panel's Current query (the server's execution receipt) |
57
+ | Bound parameter values | the same receipt |
58
+ | Row count | the resolve result |
59
+ | Hash of the complete result | hash all returned rows, not a sample |
60
+
61
+ All four must match. Sample rows are a sanity check, never the proof. If a result is too large to
62
+ hash completely, say so and treat that state as unverified.
63
+ 7. **Remove the explicit values only after that state passes.** Delete `sql` and `dataSourceId` only
64
+ once the entity owns the equivalent query and the comparison passed.
65
+ 8. **Save a named version,** so the migration is one recoverable step in version history.
66
+
67
+ ## Verifying
68
+
69
+ Read the entity back rather than trusting what you wrote:
70
+
71
+ ```bash
72
+ graphit dashboard get-entity <dashboard-id> <entity-id>
73
+ ```
74
+
75
+ Act on `entity_sql_warnings` before reporting success. A parameterized template keeps its `:name`
76
+ placeholders; save-time validation binds them before checking, so a warning is a real SQL problem,
77
+ not the placeholders.
78
+
79
+ ## When to stop
80
+
81
+ Stop, save nothing, and report the branch when:
82
+
83
+ - SQL is composed by logic you cannot fully enumerate, so you cannot list every branch.
84
+ - A code path exists that you cannot reach or trigger, so you cannot compare it.
85
+ - A result is too large to hash completely.
86
+ - Any state's four signals do not all match.
87
+
88
+ Report which query, which branch, and which signal. The user can migrate the rest and leave that one
89
+ query on the old shape, which is a legitimate end state.
@@ -1,45 +1,43 @@
1
1
  <!--
2
2
  SIZE EXEMPTION (reference file)
3
- Hard limit: 7,168 chars | Exempted ceiling: 14,600 chars
4
- Current: ~14,584 chars - intentionally over the base reference limit.
5
- Rationale: the consolidated build-time data + entity contract (live-data API, data-graphit-* entity contract, first-paint state, helper index, canonical example, version-naming discipline) - one co-load unit the co-load test forbids splitting. Loads only on HTML-deliverable turns (just-in-time, not every turn), so cache cost is bounded.
6
- Reviewed: 2026-07-09
7
- Next review: 2026-10-09
3
+ Hard limit: 7,168 chars | Exempted ceiling: set by the reference-file exemption paragraph in docs/knowledge/prompt-engineering/sizing/SIZING.md, which names this file; PE-DENY-005 mirrors it and cli/test/skill-size.test.mjs enforces it. A ceiling written only in this header is not an authority.
4
+ Rationale: one co-load unit the co-load test forbids splitting; loads only on HTML-deliverable turns.
5
+ Reviewed: 2026-07-28 | Next review: 2026-10-28
8
6
  -->
9
7
  # Canvas Runtime: Live Data and the Entity Contract
10
8
 
11
- Consult when authoring the dashboard HTML and wiring its data: how the iframe fetches live data, how every visible element becomes a platform entity, how the page paints before data arrives, and how to shape resolve SQL so filter changes stay instant. Design-system tokens and layout CSS live in `graphit-style.md`; this file owns the data wiring.
9
+ Consult when authoring dashboard HTML and wiring its data: fetching live data, making every visible element a platform entity, painting before data arrives, and shaping resolve SQL so filter changes stay instant. Design tokens and layout CSS live in `graphit-style.md`; this file owns the data wiring.
12
10
 
13
11
  ## The live-data API
14
12
 
15
- The iframe provides `graphit.resolve()` to fetch live data from cached data sources on every page load. This is how the HTML gets its data. NEVER embed query results as static JS variables (`const data = [...]`) - that freezes a snapshot that never refreshes and breaks provenance.
13
+ `graphit.resolve()` fetches live data from cached data sources on every page load. NEVER embed query results as static JS variables (`const data = [...]`) - that freezes a snapshot that never refreshes and breaks provenance.
14
+
15
+ The entity owns the query. A resolve call passes no `sql` and no `dataSourceId`: both are read from the entity wrapper, authored once in the attributes and never repeated in the call.
16
16
 
17
17
  ```js
18
18
  const result = await graphit.resolve({
19
- sql: "SELECT region, SUM(revenue) AS rev FROM ORDERS_DS GROUP BY region",
20
- dataSourceId: "ORDERS_DS",
21
19
  target: "#chart-container",
22
20
  maxRows: 10000
23
21
  });
24
22
  // Returns: { columns: string[], data: object[], rowCount: number, truncated: boolean }
25
23
  ```
26
24
 
27
- - `dataSourceId` is the data source name (the same table you SELECT FROM); its id or a unique id-prefix also works.
28
- - `target` (optional, a CSS selector or element) shows a blur and spinner overlay while loading and removes it on completion.
25
+ - `target` (CSS selector or element) does two jobs: locates the entity whose `data-graphit-sql` / `data-graphit-ds` this call runs, and shows a blur/spinner overlay while loading, removed on completion.
26
+ - `params` (optional) supplies values for the `:name` placeholders in the entity's SQL.
29
27
  - `targetEntityIds` (optional, `string[]`) - `data-graphit-id`s of OTHER graphs this result also renders into, so each one's details panel reflects filters (not just `target`); entity ids, never CSS selectors.
30
28
  - `sourceEntityId` (optional) - the graph that owns a `target`-less resolve feeding several graphs (pair with `targetEntityIds`).
31
29
  - `maxRows` (optional) defaults to **10,000**, capped at **50,000**. Aggregate to a chartable grain well under the default; raise it only for a genuine row-level export, never above the cap.
32
30
  - `result.data` is an array of row objects you render however you want.
33
31
 
34
- MUST: every resolve that feeds a rendered graph, KPI, or table carries attribution - `target` (an element inside the entity wrapper), or `sourceEntityId` plus `targetEntityIds` when one result feeds several graphs. Attribution is what records the live filtered query behind each entity's details panel; an unattributed resolve leaves that panel showing the static `data-graphit-sql` example with its baked default filters, so a user who changes a filter sees the SQL never move. Saving a page that has filters or params and zero attributed resolves returns an `unattributed_resolves` warning. Queries that feed no visual (filter option lists, freshness probes) stay unattributed.
32
+ MUST: every resolve feeding a rendered graph, KPI, or table carries attribution - `target` (an element inside the entity wrapper), or `sourceEntityId` plus `targetEntityIds` when one result feeds several graphs. Attribution records the live filtered query behind that entity's details panel; unattributed, the panel shows `data-graphit-sql` as an unrun example, so a user changing a filter sees the SQL never move. Saving a page with filters or params and zero attributed resolves returns an `unattributed_resolves` warning. Queries feeding no visual (option lists, freshness probes) stay unattributed.
35
33
 
36
- CRITICAL: use KB reference syntax (`{{metric:NAME}}`, `{{dim:NAME}}`) inside the resolve `sql` whenever a KB asset exists - the server expands it at query time, which produces the governed trust tier. See `governance.md` for the syntax and trust tiers.
34
+ CRITICAL: use KB reference syntax (`{{metric:NAME}}`, `{{dim:NAME}}`) inside the entity's `data-graphit-sql` whenever a KB asset exists - the server expands it at query time, producing the governed trust tier. Syntax and trust tiers: `governance.md`.
37
35
 
38
36
  Error handling: `graphit.resolve()` rejects on timeout (120s), bad SQL, or an invalid data source ID. Wrap calls in try/catch and show a user-visible error in the target element on failure. Verify the SQL returns data via the CLI before embedding it.
39
37
 
40
38
  ## The entity contract
41
39
 
42
- Every visible element - chart, KPI card, table, text section - must be wrapped so the platform can see it. Without `data-graphit-*` attributes the element is invisible: no click info, no @ mentions, no KB provenance. Wrapping also gives the element its 3-dot menu (hover, top-right) and details panel - data source, governed SQL, KB lineage, live results. A graph you draw and a standard `graphit.graph()` chart are equally first-class once wrapped; never rebuild a custom dashboard as native graphs to gain the menu or data sources - just add the wrapper. Each wrapped element needs ALL FOUR attributes:
40
+ Every visible element - chart, KPI card, table, text section - must be wrapped so the platform can see it. Unwrapped it is invisible: no click info, no @ mentions, no KB provenance. Wrapping also gives it the 3-dot menu (hover, top-right) and details panel - data source, governed SQL, KB lineage, live results. A graph you draw is as first-class as a `graphit.graph()` chart once wrapped; never rebuild a custom dashboard as native graphs to gain the menu, just add the wrapper. Each needs ALL FOUR attributes:
43
41
 
44
42
  ```html
45
43
  <div data-graphit-id="revenue-trend"
@@ -52,27 +50,35 @@ Every visible element - chart, KPI card, table, text section - must be wrapped s
52
50
 
53
51
  | Attribute | Format | Example |
54
52
  |-----------|--------|---------|
55
- | `data-graphit-id` | Unique kebab-case | `"spend-by-source"` |
53
+ | `data-graphit-id` | Unique; lowercase letters, digits, `-` and `_`, max 80 (kebab-case preferred) | `"spend-by-source"` |
56
54
  | `data-graphit-label` | Human-readable name | `"Ad Spend by Source"` |
57
- | `data-graphit-sql` | Executable SQL (HTML-encode the characters `<`, `>`, `&`, `"`) | `"SELECT ..."` |
55
+ | `data-graphit-sql` | The query this entity runs - executable, parameterized (HTML-encode `<`, `>`, `&`, `"`) | `"SELECT ... WHERE d = :day"` |
58
56
  | `data-graphit-ds` | Data source name (same as the FROM table) or id | `"ORDERS_DS"` |
59
57
 
60
- KB asset references are derived automatically from `{{metric:X}}` / `{{dim:X}}` templates in the SQL; the governance compiler resolves these and shows KB asset chips in the entity details panel. Missing any one attribute breaks the entity; missing the wrapper entirely makes the element invisible to the platform.
58
+ KB asset references are derived automatically from `{{metric:X}}` / `{{dim:X}}` in the SQL; the governance compiler resolves these and shows KB asset chips in the details panel. Missing any one attribute breaks the entity; missing the wrapper makes the element invisible to the platform.
59
+
60
+ **JavaScript may populate an entity, never create one.** Every entity - including on hidden tabs, collapsed panels, and lazily-shown views - exists as static markup in the HTML you save; JavaScript fills its chart host, wires listeners, and toggles visibility. Nothing server-side runs your JavaScript, so a card built at render time (`host.innerHTML = charts.map(...)`) does not exist for governance, lineage, the KB graph, `list-entities`, or a later `get-entity`. A save whose `data-graphit-id`s are reachable only by running your JavaScript is REFUSED and the error names them. A dynamic **query** is supported - the attribute carries the canonical template; a dynamic **entity** is not.
61
+
62
+ - **Declare statically, execute lazily.** A hidden card must not resolve on load - resolve a tab's entities when it first becomes visible, or a 3-tab dashboard turns 8 concurrent queries into 22 on first paint.
63
+ - **One source of truth.** The entity element owns `data-graphit-sql` and `data-graphit-ds`: the resolve/bind call reads them from the entity rather than repeating them, and a JS config array may keep rendering behavior (type, colors, height) but must never restate the chart list.
64
+ - **Never write `data-graphit-id=` in script** (selectors included) - the gate reads it as a phantom entity and refuses the save; match via `el.dataset.graphitId`.
61
65
 
62
- **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 ...`, three dots). Use the real DS table name and only columns that exist in the DS - never an invented summary column, a CTE alias, a JS variable name, or prose. If the resolve call uses a CTE, store the full WITH query. If JS builds the SQL dynamically, store one representative executable variant (for example, the default date range).
66
+ **SQL must be complete and executable.** This attribute is the query that runs, and the platform runs it again when a user opens the details panel. NEVER abbreviate, truncate, or leave an ellipsis (`FROM ...`, `SELECT ...`, three dots). Use the real DS table name and only columns that exist in the DS - never an invented summary column, a CTE alias, a JS variable name, or prose. If the query uses a CTE, store the full WITH query. Where the query is filtered, store the parameterized template with its `:name` placeholders - never a frozen variant with one date range baked in, which is the drift this contract removes. The panel's **Current query** is the server's record of what actually ran; never copy that resolved SQL back into this attribute.
63
67
 
64
68
  - **Wrong:** `data-graphit-sql="SELECT INSTALL_TIME, ROIAP_D0 FROM UA_DS"` when the DS has no `ROIAP_D0` column (the chart computes it via CASE) - the details panel errors.
65
69
  - **Right:** `data-graphit-sql="SELECT INSTALL_TIME, SUM(CASE WHEN SENIORITY=0 THEN TOTAL_IAP END)/NULLIF(SUM(COST),0) AS ROIAP_D0 FROM UA_DS GROUP BY 1"` - the same derivation the chart runs.
66
70
 
67
- **Label equals the visible title.** The `data-graphit-label` MUST match the card's visible heading exactly. Users find their chart by that label in @ mention dropdowns and entity panels - a mismatch means they cannot find it.
71
+ When the entity is filtered, `graphit.bind(el, { params, deps, render })` supplies the values - it derives SQL and data source from the bound entity the same way a resolve does. Older dashboards pass `sql` and `dataSourceId` in the call itself; that still executes and is not a defect to fix, so move a query onto its entity only when asked to.
68
72
 
69
- **Editing one existing entity.** Edit a single element surgically rather than rewriting the page: `graphit dashboard list-entities <id>` lists every entity (id, label, KB refs, data source) to find the right `data-graphit-id`; `graphit dashboard get-entity <id> <entityId>` returns just that entity's inner HTML - the exact fragment `graphit dashboard update-entity <id> <entityId>` accepts - which you change and write back. Reach for full-page `get-html` / `update-html` only when restructuring the whole layout.
73
+ **Label equals the visible title.** `data-graphit-label` MUST match the card's visible heading exactly - users find their chart by that label in @ mention dropdowns and entity panels, and a mismatch means they cannot find it.
74
+
75
+ **Editing one existing entity.** Edit surgically: `graphit dashboard list-entities <id>` lists every entity (id, label, KB refs, data source) to find the right `data-graphit-id`; `graphit dashboard get-entity <id> <entityId>` returns just that entity's inner HTML - the exact fragment `graphit dashboard update-entity <id> <entityId>` accepts - which you change and write back. Use full-page `get-html` / `update-html` only when restructuring the layout.
70
76
 
71
77
  **Name every version.** Always pass `--label "<what changed>"` on every `update-html` / `update-entity` (e.g. `--label "Added revenue KPI row"`) - it names the version in the dashboard's history so edits stay traceable. Keep it short; no secrets or SQL dumps.
72
78
 
73
79
  ## First-paint loading state
74
80
 
75
- The dashboard HTML paints before the SDK connects (iframe load plus handshake), so the SDK's own spinner cannot cover the first moments. Bake a pure-CSS overlay into the HTML so every chart shows a spinner from the first frame; the SDK adopts that overlay and removes it when the element's `graphit.resolve()` settles (success or error).
81
+ The HTML paints before the SDK connects, so the SDK's own spinner cannot cover the first moments. Bake a pure-CSS overlay into the HTML so every chart spins from the first frame; the SDK adopts it and removes it when that element's `graphit.resolve()` settles (success or error).
76
82
 
77
83
  Add once to the page `<style>`:
78
84
 
@@ -83,7 +89,7 @@ Add once to the page `<style>`:
83
89
  .gh-loading-spin{animation:gh-spin .7s linear infinite}
84
90
  ```
85
91
 
86
- Add the overlay inside EVERY element passed as `target:` to `graphit.resolve()` - and ONLY those elements. A static text or title section with no resolve call would spin forever.
92
+ Add the overlay inside EVERY element passed as `target:` to `graphit.resolve()` - and ONLY those. A static text or title section with no resolve call would spin forever.
87
93
 
88
94
  ```html
89
95
  <div id="spend-chart" class="gh-loading">
@@ -95,7 +101,7 @@ The class names are a contract with the SDK (`gh-loading`, `gh-loading-overlay`,
95
101
 
96
102
  ## Cache-friendly resolve SQL
97
103
 
98
- A resolve query that follows these shapes serves from a semantic cache in roughly 10ms on filter changes instead of a full DuckDB recompute (5 to 37 seconds on wide data sources). Write resolve SQL in this style by default.
104
+ A resolve query following these shapes serves from a semantic cache in roughly 10ms on filter changes instead of a full DuckDB recompute (5 to 37s on wide data sources). Write resolve SQL this way by default.
99
105
 
100
106
  **Shapes that hit the cache:**
101
107
 
@@ -113,25 +119,25 @@ A resolve query that follows these shapes serves from a semantic cache in roughl
113
119
  - `OR` or `NOT` in WHERE.
114
120
  - Ratio metrics (`SUM(a)/NULLIF(SUM(b),0)`) - compute client-side or use two resolves. To display a ratio as a percent, multiply by 100 in SQL (`* 100.0 ... AS x_pct`): the `"percent"` format only appends `%`, it does not scale, so a 0-to-1 ratio would otherwise show as `0.42%`, not `42%`.
115
121
  - `CURRENT_DATE`-relative predicates.
116
- - Top-N with the aggregate only in ORDER BY (`... GROUP BY dim ORDER BY SUM(metric) DESC LIMIT N` with no decomposable aggregate in SELECT).
122
+ - Top-N with the aggregate only in ORDER BY.
117
123
 
118
- **Fresh anchors.** When a query needs a data-driven anchor - the latest install date, a "days since" reference, a MAX(date) - never fetch it once at page load into a JS variable and reuse it across refreshes. A long-open tab silently goes stale and every maturity gate or rolling window computed against it drifts. Re-resolve the anchor inside each refresh cycle, or derive it in a subquery so the query always anchors to the current data.
124
+ **Fresh anchors.** When a query needs a data-driven anchor - latest install date, a "days since" reference, a MAX(date) - never fetch it once at page load into a JS variable and reuse it across refreshes. A long-open tab silently goes stale and every maturity gate or rolling window computed against it drifts. Re-resolve the anchor inside each refresh cycle, or derive it in a subquery.
119
125
 
120
126
  ## Rate-limit budget
121
127
 
122
- `graphit.resolve()` is rate-limited to 120 requests per minute per user per dashboard. Each call counts as one request. Design for that budget:
128
+ `graphit.resolve()` is rate-limited to 120 requests per minute per user per dashboard. Each call counts as one. Design for that budget:
123
129
 
124
- - **Single refresh function.** Put all queries in ONE `Promise.all` inside one `refresh()` function so they share the same time window. NEVER scatter `graphit.resolve()` across independent event handlers or timeouts - that turns one user action into several bursts.
125
- - **Count queries per interaction.** 6 charts is 6 requests per filter change, about 20 changes per minute of budget; 12 charts is about 10 changes per minute. With 10 or more charts and 3 or more filters, debounce filter changes (300ms) so rapid clicks do not each trigger a full refresh.
126
- - **Reuse trend data for KPIs.** If you already fetch a weekly time series, derive the KPI total and its sparkline from that result in JS instead of running a separate aggregate query - one query serves both. Anchor the extra graphs it feeds with `targetEntityIds` per the resolve attribution rule above. Canonical KPI-row example: `kpi.md`.
127
- - **Avoid redundant refreshes.** If a filter affects only some charts, split into targeted refresh functions (`refreshKPIs()`, `refreshCharts()`) so unchanged sections do not re-query.
130
+ - **Single refresh function.** Put all queries in ONE `Promise.all` inside one `refresh()` so they share a time window. NEVER scatter `graphit.resolve()` across independent event handlers or timeouts - that turns one user action into several bursts.
131
+ - **Count queries per interaction.** 6 charts is 6 requests per filter change, about 20 changes per minute of budget; 12 charts is about 10. With 10 or more charts and 3 or more filters, debounce filter changes (300ms).
132
+ - **Reuse trend data for KPIs.** If you already fetch a weekly time series, derive the KPI total and its sparkline from that result in JS instead of a separate aggregate query. Anchor the extra graphs it feeds with `targetEntityIds` per the attribution rule above. Canonical KPI-row example: `kpi.md`.
133
+ - **Avoid redundant refreshes.** If a filter affects only some charts, split into targeted refresh functions (`refreshKPIs()`, `refreshCharts()`).
128
134
  - **No polling.** NEVER use `setInterval(refresh, ...)`. Data sources update on their own schedule; a polling dashboard burns the entire budget.
129
135
 
130
136
  If you hit the limit, the API returns a "Too many requests" error with a retry-after hint.
131
137
 
132
138
  ## Helper index
133
139
 
134
- You have full creative freedom: draw with `type:'custom'` or inline SVG/CSS/HTML tables for full control. The helpers below are shortcuts, not requirements: when a documented type fits, use it; otherwise hand-roll immediately - do not deliberate.
140
+ You have full creative freedom: draw with `type:'custom'` or inline SVG/CSS/HTML tables. The helpers below are shortcuts, not requirements: when a documented type fits, use it; otherwise hand-roll immediately - do not deliberate.
135
141
 
136
142
  | Helper | What it renders | Depth |
137
143
  |---|---|---|
@@ -142,32 +148,6 @@ You have full creative freedom: draw with `type:'custom'` or inline SVG/CSS/HTML
142
148
  | `graphit.presentation(el)` | A full-screen slide deck builder | `presentations.md` |
143
149
  | `graphit.filter / param / dateRange / cascade / bind` | Headless interactivity (zero imposed markup) | `filters.md`, `filters-advanced.md` |
144
150
 
145
- **Standard `graphit.graph` types (set `config.type`):** `"bar"`, `"horizontal-bar"` (alias `"hbar"`), `"line"`, `"area"`, `"donut"`, `"pie"` (alias of `donut`), `"scatter"` (alias `"bubble"`), `"stacked-bar"` (alias `"stacked"`), `"heatmap"`, `"funnel"`, `"gauge"`, `"sparkline"`, plus `"custom"` (bespoke `draw`). The full per-type config (axes, dual axis, `valueFormat`, the non-scaling percent rule, the custom `ctx` contract, and hand-rolled shapes) lives in `chart-patterns.md`. Saved org templates register as types too.
146
-
147
- **Logic versus styling.** `filter`, `param`, `bind`, `dateRange`, `cascade` are headless logic with zero imposed styling - you own the markup. A standard `graphit.graph` type, `table`, `kpi`, `presentation` render a fixed house style. Two trade-offs to surface to the user: a control persists to saved views ONLY when registered with `graphit.filter` / `param` / `dateRange` (a hand-rolled `<select>` will not save), and a standard chart type cannot be deeply restyled - for a custom look use `graphit.graph(el, {type:'custom', draw})` (see `chart-patterns.md`) or hand-draw SVG/CSS, still fetching data via `graphit.resolve`.
151
+ **Standard `graphit.graph` types (set `config.type`):** `"bar"`, `"horizontal-bar"` (alias `"hbar"`), `"line"`, `"area"`, `"donut"`, `"pie"` (alias of `donut`), `"scatter"` (alias `"bubble"`), `"stacked-bar"` (alias `"stacked"`), `"heatmap"`, `"funnel"`, `"gauge"`, `"sparkline"`, plus `"custom"`. Full per-type config (axes, dual axis, `valueFormat`, the non-scaling percent rule, the custom `ctx` contract, hand-rolled shapes) lives in `chart-patterns.md`. Saved org templates register as types too.
148
152
 
149
- ## Canonical entity with live data
150
-
151
- ```html
152
- <div data-graphit-id="spend-by-source"
153
- data-graphit-label="Ad Spend by Source"
154
- data-graphit-sql="SELECT {{dim:MEDIA_SOURCE_DIMENSION}} AS source, {{metric:TOTAL_AD_SPEND}} AS spend FROM MARKETING_UA_DS GROUP BY source ORDER BY spend DESC"
155
- data-graphit-ds="MARKETING_UA_DS">
156
- <div id="spend-chart" class="gh-loading">
157
- <!-- gh-loading-overlay spinner from the First-paint section -->
158
- </div>
159
- </div>
160
- <script>
161
- (async function() {
162
- var r = await graphit.resolve({
163
- sql: "SELECT MEDIA_SOURCE, SUM(APPSFLYER_COST) AS spend FROM MARKETING_UA_DS GROUP BY MEDIA_SOURCE ORDER BY spend DESC",
164
- dataSourceId: "MARKETING_UA_DS",
165
- target: "#spend-chart"
166
- });
167
- graphit.graph("#spend-chart", {
168
- type: "bar", data: r.data, x: "MEDIA_SOURCE", y: "spend",
169
- title: "Ad Spend by Source", valueFormat: "currency"
170
- });
171
- })();
172
- </script>
173
- ```
153
+ **Logic versus styling.** `filter`, `param`, `bind`, `dateRange`, `cascade` are headless - you own the markup. `graphit.graph` types, `table`, `kpi`, `presentation` render a fixed house style. Surface two trade-offs to the user: a control persists to saved views ONLY when registered with `graphit.filter` / `param` / `dateRange` (a hand-rolled `<select>` will not save), and a standard chart type cannot be deeply restyled - for a custom look use `type:'custom'` or hand-draw SVG/CSS, still fetching via `graphit.resolve`.