@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.
Files changed (61) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.codex-plugin/plugin.json +3 -2
  4. package/README.md +99 -0
  5. package/bin/graphit +45 -0
  6. package/bin/graphit.cmd +8 -0
  7. package/bin/graphit.ps1 +38 -0
  8. package/dist/api/client.js +11 -0
  9. package/dist/api/client.js.map +1 -1
  10. package/dist/commands/auth.js +14 -6
  11. package/dist/commands/auth.js.map +1 -1
  12. package/dist/commands/ds-poll.d.ts +8 -0
  13. package/dist/commands/ds-poll.js +53 -0
  14. package/dist/commands/ds-poll.js.map +1 -0
  15. package/dist/commands/ds.js +113 -57
  16. package/dist/commands/ds.js.map +1 -1
  17. package/dist/commands/governance.js +15 -11
  18. package/dist/commands/governance.js.map +1 -1
  19. package/dist/commands/kb.js +14 -4
  20. package/dist/commands/kb.js.map +1 -1
  21. package/dist/commands/query.js +9 -7
  22. package/dist/commands/query.js.map +1 -1
  23. package/dist/index.js +2 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/output/format.d.ts +1 -0
  26. package/dist/output/format.js +6 -0
  27. package/dist/output/format.js.map +1 -1
  28. package/dist/output/table.d.ts +1 -0
  29. package/dist/output/table.js +20 -3
  30. package/dist/output/table.js.map +1 -1
  31. package/dist/skill-version.d.ts +12 -0
  32. package/dist/skill-version.js +21 -0
  33. package/dist/skill-version.js.map +1 -1
  34. package/dist/update-check.d.ts +19 -0
  35. package/dist/update-check.js +105 -12
  36. package/dist/update-check.js.map +1 -1
  37. package/hooks/hooks.json +13 -0
  38. package/package.json +11 -3
  39. package/scripts/block-legacy-setup.mjs +90 -0
  40. package/scripts/generate-commands-doc.mjs +182 -0
  41. package/scripts/plugin-status.mjs +179 -29
  42. package/scripts/sync-plugin-version.mjs +26 -4
  43. package/skills/graphit/SKILL.md +172 -433
  44. package/skills/graphit/VERSION.json +1 -1
  45. package/skills/graphit/cursor/README.md +11 -0
  46. package/skills/graphit/graphit.mdc +3 -1
  47. package/skills/graphit/references/dashboard-planning.md +1 -7
  48. package/skills/graphit/references/data-sources.md +30 -1
  49. package/skills/graphit/references/filters-advanced.md +61 -0
  50. package/skills/graphit/references/filters.md +27 -93
  51. package/skills/graphit/references/governance.md +49 -66
  52. package/skills/graphit/references/graphit-style.md +6 -18
  53. package/skills/graphit/references/kb-actions.md +44 -52
  54. package/skills/graphit/references/kb-discovery.md +10 -13
  55. package/skills/graphit/references/kb-structure.md +10 -8
  56. package/skills/graphit/references/kb-traversal.md +16 -16
  57. package/skills/graphit/references/operations.md +115 -0
  58. package/skills/graphit/references/parameterized-metrics.md +32 -44
  59. package/skills/graphit/references/runtime.md +172 -0
  60. package/skills/graphit/references/sql-reference.md +5 -42
  61. package/skills/graphit/references/domain-lenses.md +0 -116
@@ -0,0 +1,172 @@
1
+ <!--
2
+ SIZE EXEMPTION (reference file)
3
+ Hard limit: 7,168 chars | Exempted ceiling: 14,336 chars
4
+ Current: 12,478 chars (content) - intentionally over the base reference limit.
5
+ Rationale: runtime.md is the consolidated build-time data + entity contract. Its core - the live-data API, the data-graphit-* entity contract, the first-paint loading state, the helper index, and the canonical example - is a single co-load unit: building one live element needs all of it together, so by the co-load test (split a reference only where the agent needs one part without the other) it cannot be split without creating refs that always load together, which the test forbids. The one use-case-clean seam (cache-friendly resolve SQL + the rate-limit budget, needed only for interactive or large dashboards) was deliberately kept inline to preserve the single authoritative contract. This ref loads only on HTML-deliverable turns (just-in-time, not every turn), so the cache cost is bounded.
6
+ Reviewed: 2026-06-17
7
+ Next review: 2026-09-17
8
+ -->
9
+ # Canvas Runtime: Live Data and the Entity Contract
10
+
11
+ Consult when authoring the dashboard HTML and wiring its data. This is the build-time contract: 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.
12
+
13
+ ## Contents
14
+
15
+ - The live-data API (`graphit.resolve`)
16
+ - The entity contract (`data-graphit-*`)
17
+ - First-paint loading state
18
+ - Cache-friendly resolve SQL
19
+ - Rate-limit budget
20
+ - Helper index (chart / table / kpi / presentation / filters)
21
+ - Canonical entity with live data
22
+
23
+ ## The live-data API
24
+
25
+ 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.
26
+
27
+ ```js
28
+ const result = await graphit.resolve({
29
+ sql: "SELECT region, SUM(revenue) AS rev FROM ORDERS_DS GROUP BY region",
30
+ dataSourceId: "ds_abc123",
31
+ target: "#chart-container",
32
+ maxRows: 10000
33
+ });
34
+ // Returns: { columns: string[], data: object[], rowCount: number, truncated: boolean }
35
+ ```
36
+
37
+ - `dataSourceId` is the ID from `graphit ds list`.
38
+ - `target` (optional, a CSS selector or element) shows a blur and spinner overlay while loading and removes it on completion.
39
+ - `maxRows` (optional) defaults to **10,000 rows** and may be raised up to a cap of **50,000 rows**. A dashboard query should aggregate to a chartable grain well under the default; reach for a higher value only for a genuine row-level export, and never above the cap.
40
+ - `result.data` is an array of row objects you render however you want.
41
+
42
+ 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. The canonical example at the end of this file shows a resolve call with reference syntax in context.
43
+
44
+ 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 message in the target element on failure. Verify the SQL returns data via the CLI before embedding it in HTML.
45
+
46
+ ## The entity contract
47
+
48
+ 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 to the platform: no click info, no @ mentions, no KB provenance. Each wrapped element needs ALL FOUR attributes:
49
+
50
+ ```html
51
+ <div data-graphit-id="revenue-trend"
52
+ data-graphit-label="Revenue Trend"
53
+ data-graphit-sql="SELECT {{dim:REGION}} AS region, {{metric:REVENUE}} AS revenue FROM ORDERS_DS GROUP BY region"
54
+ data-graphit-ds="ds_abc123">
55
+ <!-- chart, KPI, or table content here -->
56
+ </div>
57
+ ```
58
+
59
+ | Attribute | Format | Example |
60
+ |-----------|--------|---------|
61
+ | `data-graphit-id` | Unique kebab-case | `"spend-by-source"` |
62
+ | `data-graphit-label` | Human-readable name | `"Ad Spend by Source"` |
63
+ | `data-graphit-sql` | Executable SQL (HTML-encode the characters `<`, `>`, `&`, `"`) | `"SELECT ..."` |
64
+ | `data-graphit-ds` | Data source ID from `graphit ds list` | `"ds_abc123"` |
65
+
66
+ 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.
67
+
68
+ **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).
69
+
70
+ - **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.
71
+ - **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.
72
+
73
+ **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.
74
+
75
+ ## First-paint loading state
76
+
77
+ 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).
78
+
79
+ Add once to the page `<style>`:
80
+
81
+ ```css
82
+ @keyframes gh-spin{to{transform:rotate(360deg)}}
83
+ .gh-loading{position:relative;min-height:120px}
84
+ .gh-loading-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;z-index:9998;backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);background:color-mix(in srgb,var(--graphit-surface-raised,#fff) 50%,transparent);border-radius:inherit}
85
+ .gh-loading-spin{animation:gh-spin .7s linear infinite}
86
+ ```
87
+
88
+ 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.
89
+
90
+ ```html
91
+ <div id="spend-chart" class="gh-loading">
92
+ <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>
93
+ </div>
94
+ ```
95
+
96
+ The class names are a contract with the SDK (`gh-loading`, `gh-loading-overlay`, `gh-loading-spin`, `gh-spin`) - keep them exactly as shown or the SDK cannot adopt and remove the overlay. NEVER write text placeholders ("Loading", "Fetching data") - they never animate and make slow loads look stuck.
97
+
98
+ ## Cache-friendly resolve SQL
99
+
100
+ 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.
101
+
102
+ **Shapes that hit the cache:**
103
+
104
+ - Single table (no JOIN or UNION).
105
+ - WHERE as a flat AND of `column = literal`, `column IN (...)`, `column BETWEEN ... AND ...` conjuncts.
106
+ - Bare aggregates only: `SUM(col)`, `COUNT(*)`, `MIN(col)`, `MAX(col)`. No wrapping functions (`ROUND(SUM(x))`), no aggregate arithmetic (`SUM(a)/NULLIF(SUM(b),0)`), no `AVG`.
107
+ - Literal dates (`>= '2026-01-01'`), never `CURRENT_DATE` expressions.
108
+ - GROUP BY column names or ordinals; ORDER BY and LIMIT allowed (outer query only).
109
+ - CTEs are fine when the CTE body follows the same rules.
110
+ - Top-N rank queries: project the sort metric in SELECT (`SELECT dim, SUM(metric) AS rv ... ORDER BY rv DESC LIMIT N`), not only in ORDER BY.
111
+
112
+ **Shapes that skip the cache** (they still run correctly, just slower):
113
+
114
+ - `COUNT(DISTINCT x)`, window functions, HAVING, QUALIFY.
115
+ - `OR` or `NOT` in WHERE.
116
+ - 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%`.
117
+ - `CURRENT_DATE`-relative predicates.
118
+ - 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).
119
+
120
+ ## Rate-limit budget
121
+
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:
123
+
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.
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.
128
+ - **No polling.** NEVER use `setInterval(refresh, ...)`. Data sources update on their own schedule; a polling dashboard burns the entire budget.
129
+
130
+ If you hit the limit, the API returns a "Too many requests" error with a retry-after hint.
131
+
132
+ ## Helper index
133
+
134
+ You have full creative freedom: build charts with inline SVG, CSS, and HTML tables for full control. The iframe also provides convenience helpers for quick standard output. These are shortcuts, not requirements.
135
+
136
+ | Helper | What it renders | Depth |
137
+ |---|---|---|
138
+ | `graphit.chart(el, {type, data, x, y, ...})` | A native chart of the given `type` | `chart-patterns.md` (per-type config) |
139
+ | `graphit.table(el, {data, columns?, columnFormats?})` | A styled HTML table; `columnFormats` maps a column name to `"currency"` / `"percent"` / `"number"` | `chart-patterns.md` |
140
+ | `graphit.kpi(el, {value, label?, format?})` | A KPI card with an optional delta | `chart-patterns.md` |
141
+ | `graphit.presentation(el)` | A full-screen slide deck builder | `presentations.md` |
142
+ | `graphit.filter / param / dateRange / cascade / bind` | Headless interactivity (zero imposed markup) | `filters.md`, `filters-advanced.md` |
143
+
144
+ **Native `graphit.chart` types:** `"bar"`, `"horizontal-bar"` (alias `"hbar"` - use when category labels are long), `"line"`, `"area"`, `"donut"`, `"pie"` (alias of `donut`), `"scatter"` (alias `"bubble"`), `"stacked-bar"` (alias `"stacked"`), `"heatmap"`, `"funnel"`, `"gauge"`, `"sparkline"`. The full per-type config (axes, dual axis, `valueFormat`, the non-scaling percent rule, and the hand-rolled shapes such as treemap and sankey) lives in `chart-patterns.md`. Saved org templates register as types too.
145
+
146
+ **Logic versus styling.** `filter`, `param`, `bind`, `dateRange`, `cascade` are headless logic with zero imposed styling - you own the markup. `chart`, `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 `graphit.chart` cannot be deeply restyled - hand-draw with SVG and CSS for a custom look while still fetching data via `graphit.resolve`.
147
+
148
+ ## Canonical entity with live data
149
+
150
+ ```html
151
+ <div data-graphit-id="spend-by-source"
152
+ data-graphit-label="Ad Spend by Source"
153
+ 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"
154
+ data-graphit-ds="ds_abc123">
155
+ <div id="spend-chart" class="gh-loading">
156
+ <!-- gh-loading-overlay spinner from the First-paint section -->
157
+ </div>
158
+ </div>
159
+ <script>
160
+ (async function() {
161
+ var r = await graphit.resolve({
162
+ sql: "SELECT MEDIA_SOURCE, SUM(APPSFLYER_COST) AS spend FROM MARKETING_UA_DS GROUP BY MEDIA_SOURCE ORDER BY spend DESC",
163
+ dataSourceId: "ds_abc123",
164
+ target: "#spend-chart"
165
+ });
166
+ graphit.chart("#spend-chart", {
167
+ type: "bar", data: r.data, x: "MEDIA_SOURCE", y: "spend",
168
+ title: "Ad Spend by Source", valueFormat: "currency"
169
+ });
170
+ })();
171
+ </script>
172
+ ```
@@ -111,34 +111,13 @@ SELECT UNNEST(generate_series(
111
111
 
112
112
  The outer SELECT can ONLY reference columns the subquery exposes (its aliases). Base-table columns aggregated away in the subquery do NOT exist at the outer level.
113
113
 
114
- ## Cache-Friendly SQL (Canvas Resolve)
114
+ ## Cache-friendly resolve SQL
115
115
 
116
- Canvas `graphit.resolve()` queries that follow these shapes serve from a semantic cache in ~10ms on filter changes instead of a full DuckDB recompute (5-37s on wide data sources). Write resolve SQL in this style by default.
116
+ When the query feeds a canvas `graphit.resolve()` call, write it in the cache-friendly shape so filter changes stay instant. The shape rules and the shapes that skip the cache live in `runtime.md`.
117
117
 
118
- **Shape rules:**
119
- - Single table (no JOIN/UNION)
120
- - WHERE as flat AND of `column = literal`, `column IN (...)`, `column BETWEEN ... AND ...` conjuncts
121
- - Bare aggregates only: `SUM(col)`, `COUNT(*)`, `MIN(col)`, `MAX(col)` - no wrapping functions (`ROUND(SUM(x))`), no aggregate arithmetic (`SUM(a)/NULLIF(SUM(b),0)`), no `AVG` (v2)
122
- - Literal dates (`>= '2026-01-01'`), never `CURRENT_DATE` expressions
123
- - GROUP BY column names or ordinals; ORDER BY / LIMIT allowed (outer only)
124
- - CTEs are fine when the CTE body follows the same rules
125
- - Top-N rank queries: project the sort metric in SELECT (`SELECT dim, SUM(metric) AS rv ... ORDER BY rv DESC LIMIT N`), not only in ORDER BY
118
+ ## Data source routing
126
119
 
127
- **Shapes that skip the cache** (fall back to normal execution, still correct):
128
- - `COUNT(DISTINCT x)`, window functions, HAVING, QUALIFY
129
- - OR / NOT in WHERE
130
- - Ratio metrics (`SUM(a)/NULLIF(SUM(b),0)`) - compute client-side or use two resolves. To display as a percent, multiply by 100 (`* 100.0 ... AS x_pct`): the `"percent"` format only appends `%`, it does not scale (a 0-1 ratio would show as `0.42%`, not `42%`).
131
- - `CURRENT_DATE`-relative predicates
132
- - Top-N with aggregate only in ORDER BY (`SELECT dim FROM t GROUP BY dim ORDER BY SUM(metric) DESC LIMIT N` - no decomposable aggregate in SELECT)
133
-
134
- ## Data Source Routing
135
-
136
- | Situation | Command | Speed |
137
- |---|---|---|
138
- | Table has a cached data source | `graphit query "SQL" --ds <id>` | ~100ms, DuckDB |
139
- | No data source | `graphit query "SQL" --warehouse --connection <id>` | ~10s, Snowflake |
140
-
141
- Always prefer cached data sources. Check with `graphit ds list`. If no data source covers the table, suggest creating one for future speed.
120
+ Always prefer a cached data source (`graphit query "SQL" --ds <id>`, roughly 100ms via DuckDB) over a live warehouse query (`graphit query "SQL" --warehouse --connection <id>`, roughly 10s via Snowflake). The full routing table, the `ds list` output template, and the source-shape guidance live in `data-sources.md`.
142
121
 
143
122
  ## Presenting Query Results
144
123
 
@@ -210,20 +189,4 @@ For ad-hoc queries, suggest the KB reference equivalent when one exists. This nu
210
189
 
211
190
  Zero rows: explain what you checked and hypothesize why (wrong date range, filter too strict, table empty).
212
191
 
213
- ## Presenting Data Source Results
214
-
215
- After `graphit ds list`:
216
-
217
- ~~~
218
- **3 data sources:**
219
-
220
- | Name | ID | Rows | Status | Governed |
221
- |---|---|---:|---|---|
222
- | **MARKETING_UA_DS** | ds_abc123 | 1,247,832 | active | yes |
223
- | **PLAYER_QUALITY** | ds_def456 | 892,104 | active | no |
224
- | **REVENUE_EVENTS** | ds_ghi789 | 3,412,006 | stale | yes |
225
-
226
- Using **MARKETING_UA_DS** (ds_abc123) which covers spend, installs, and ROAS columns.
227
- ~~~
228
-
229
- End with a recommendation of which DS to use, or note if none covers the needed table.
192
+ The `graphit ds list` output template lives in `data-sources.md`.
@@ -1,116 +0,0 @@
1
- # Domain Lenses
2
-
3
- Consult when the user's data signals match a specific business domain. Each lens provides domain-specific metrics, chart types, and anti-patterns that make dashboards feel expert-built.
4
-
5
- Detect the domain from column names and user intent. Apply universal planning rules first (see dashboard-planning.md), then layer domain-specific patterns.
6
-
7
- Several high-value lens charts - **waterfall** (P&L / ARR bridges), **bullet** (KPI vs target), **Lorenz** (revenue concentration), and **histogram** (attainment distribution) - are NOT native `graphit.chart()` types, so `graphit.chart()` throws on those names. Build them as hand-rolled SVG (see `chart-selection.md` for the native-vs-hand-rolled split). The native lens charts (line, bar, stacked-bar, heatmap, funnel, KPI, sparkline) render through `graphit.chart` / `graphit.kpi`.
8
-
9
- ---
10
-
11
- ## Marketing & Attribution
12
-
13
- **Signals:** utm_*, channel, creative_id, spend, cpi, impressions, clicks, roas, cac, ltv, attribution_*, skan_*, conversions, ctr.
14
-
15
- **Key metrics:**
16
-
17
- | Metric | Formula pattern | Chart |
18
- |---|---|---|
19
- | Blended CAC / Paid CAC | spend / new_customers | bar/KPI, target + YoY |
20
- | ROAS D7/D30/D90 | revenue_D0-N / spend | cumulative cohort line + 100% breakeven |
21
- | MER | revenue / marketing_spend | KPI |
22
- | LTV:CAC Ratio | LTV / CAC | bar, floor 3:1 |
23
- | Payback Period | days to recoup CAC | cumulative line |
24
-
25
- **Must-have charts:** Cumulative cohort ROAS curve with 100% breakeven line. Channel-mix sorted bar (never pie for 6+). Creative scorecard table. Attribution model comparison stacked bar.
26
-
27
- **Anti-patterns:** Mismatched attribution windows across channels (7d vs 30d = 30% artifact). ROAS without payback context. Summing self-reported channel conversions without dedup (totals 110-180%). Spend without incrementality context. Creative table without refresh ratio.
28
-
29
- **Clarify first:** Attribution model + window? ROAS gross or margin-adjusted?
30
-
31
- ---
32
-
33
- ## Financial / Executive
34
-
35
- **Signals:** revenue, mrr, arr, cogs, gross_margin, opex, ebitda, budget, forecast, actual, variance, runway, burn, cash_balance, gl_account.
36
-
37
- **Key metrics:**
38
-
39
- | Metric | Formula pattern | Chart |
40
- |---|---|---|
41
- | P&L Cascade | Rev->COGS->GP->Opex->EBITDA->Net | waterfall |
42
- | Variance $/% | Actual-Plan, sign-aware per line type | semantic-color bar |
43
- | GM% by segment | (Rev-COGS)/Rev per product/cohort | line + small multiples |
44
- | Burn Multiple | net_burn / net_new_ARR | KPI + trend |
45
- | Runway Months | Cash / abs(Net Burn) | KPI color-banded |
46
- | Rule of 40 | Growth% + EBITDA margin% | KPI + trend |
47
-
48
- **Must-have charts:** Waterfall (P&L, variance bridge, ARR bridge). Bullet chart for KPI vs target. Plan vs Forecast vs Actual (3 series, never 2). Variance bars with semantic color (green=favorable, red=unfavorable regardless of direction).
49
-
50
- **Anti-patterns:** Direction-based green/red instead of favorable/unfavorable. Blended GM hiding AI inference cost compression. YTD comparison in Jan/Dec (use TTM). Cash burn without runway. Revenue without ASC 606 distinction. "Three-line" missing Forecast (Plan vs Actual vs Forecast = 3).
51
-
52
- **Clarify first:** Revenue = bookings, ARR, or GAAP recognized?
53
-
54
- ---
55
-
56
- ## Product / Growth
57
-
58
- **Signals:** install_date, signup_at, cohort_*, retention_d*, dau, mau, activation_*, payer_*, funnel_step, session_*, ltv, arpu, arpdau.
59
-
60
- **Key metrics:**
61
-
62
- | Metric | Formula pattern | Chart |
63
- |---|---|---|
64
- | D1/D7/D30 Retention | active_dN / cohort_size | heatmap or overlay |
65
- | Stickiness | DAU/MAU | line + ref bands |
66
- | ARPDAU | revenue_day / DAU_day | line + 7d MA |
67
- | Payer Conversion D7 | first_payers_d7 / cohort | cumulative line |
68
- | Whale Share | sum(rev) top-X% / total_rev | Lorenz / decile bar |
69
- | Activation Rate | reached_aha / signups | cohort heatmap |
70
-
71
- **Must-have charts:** Cohort retention heatmap (X=days since signup, Y=cohort, color=retention%). Cumulative cohort overlay (max 6 series). Lorenz/decile bar for revenue concentration. FTUE funnel with per-cohort drift.
72
-
73
- **Anti-patterns:** Period retention instead of cohort retention. Bare ARPU without distribution (whales break every mean). Mean on heavy-tailed metrics (use median + p90). LTV without cohort horizon day. Vanity cumulative tiles.
74
-
75
- ---
76
-
77
- ## Operational Monitoring
78
-
79
- **Signals:** latency, p50/p95/p99, error_rate, slo, uptime, mttr, throughput, oee, cycle_time, otif, fill_rate, stockout, deployment_frequency.
80
-
81
- **Key metrics:**
82
-
83
- | Metric | Formula pattern | Chart |
84
- |---|---|---|
85
- | Latency p50/p95/p99 | percentile buckets | multi-percentile line + SLO |
86
- | Error Budget | 100% - (errors/total) rolling 28d | bullet chart |
87
- | DORA Quartet | deploy freq, lead time, CFR, recovery | 4-tile KPI |
88
- | OEE | Availability x Performance x Quality | KPI + 3-tile decomp |
89
- | OTIF | on-time AND in-full | trend + decomp stacked bar |
90
-
91
- **Must-have charts:** Multi-percentile line + SLO threshold band. Bullet chart (not gauge). Latency heatmap. Error sources Pareto.
92
-
93
- **Anti-patterns:** Averages without percentiles. KPI without threshold band. Vanity uptime % (use error-budget framing). Aggregating across segments (global 0.4% hides a region at 8%).
94
-
95
- ---
96
-
97
- ## Sales / Pipeline
98
-
99
- **Signals:** opp_id, stage, amount, close_date, forecast_category, mql, sql, arr, acv, tcv, quota, attainment, loss_reason, expansion_arr.
100
-
101
- **Key metrics:**
102
-
103
- | Metric | Formula pattern | Chart |
104
- |---|---|---|
105
- | Pipeline Coverage | open_pipe / quota_remaining | KPI, target=1/win_rate |
106
- | Sales Velocity | (opps x win_rate x avg_acv) / cycle_days | KPI + trend |
107
- | Win Rate by ACV | won / (won+lost) per band | sorted bar, min-N 30 |
108
- | Quota Attainment | closed_won / quota | histogram (bimodal!) |
109
- | Net New ARR | new + expansion - contraction - churn | waterfall |
110
- | Forecast MAPE | mean(abs(actual-forecast)/actual) | line + target |
111
-
112
- **Must-have charts:** Net New ARR waterfall (start + new + expansion - contraction - churn = end). Forecast convergence curve (13 weeks). Coverage curve by quarter-week. Win rate by ACV band.
113
-
114
- **Anti-patterns:** Fixed 3x coverage instead of 1/win_rate per segment. ACV/ARR/TCV/bookings conflated. Quota attainment as bare mean (bimodal - use histogram). Stage-weighted pipe using CRM defaults (30-50% overstate). "No decision" lumped (40-60% of B2B - split it).
115
-
116
- **Clarify first:** MQL/SQL definitions (same column = 13-40% conversion depending on org). Revenue = bookings vs ARR vs GAAP. Coverage target: derive from historical win rate per segment, not fixed multiple.