@graphit/cli 0.2.208 → 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.
@@ -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`.