@graphit/cli 0.2.114 → 0.2.140

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 (45) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +2 -0
  5. package/bin/graphit +1 -1
  6. package/bin/graphit.ps1 +1 -1
  7. package/commands/doctor.md +14 -0
  8. package/commands/help.md +26 -0
  9. package/commands/login.md +13 -0
  10. package/commands/logout.md +11 -0
  11. package/commands/update.md +16 -0
  12. package/dist/commands/dashboard.js +126 -3
  13. package/dist/commands/dashboard.js.map +1 -1
  14. package/dist/commands/ds-config.d.ts +45 -0
  15. package/dist/commands/ds-config.js +122 -0
  16. package/dist/commands/ds-config.js.map +1 -0
  17. package/dist/commands/ds.d.ts +0 -45
  18. package/dist/commands/ds.js +2 -126
  19. package/dist/commands/ds.js.map +1 -1
  20. package/dist/commands/governance.js +1 -17
  21. package/dist/commands/governance.js.map +1 -1
  22. package/dist/commands/kb-create.js +2 -2
  23. package/dist/commands/kb-create.js.map +1 -1
  24. package/dist/commands/kb-update.js +2 -2
  25. package/dist/commands/kb-update.js.map +1 -1
  26. package/dist/commands/query.d.ts +38 -0
  27. package/dist/commands/query.js +138 -11
  28. package/dist/commands/query.js.map +1 -1
  29. package/dist/index.js +2 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/skill-guard.d.ts +4 -0
  32. package/dist/skill-guard.js +75 -0
  33. package/dist/skill-guard.js.map +1 -0
  34. package/package.json +2 -1
  35. package/scripts/plugin-status.mjs +49 -0
  36. package/skills/graphit/SKILL.md +15 -12
  37. package/skills/graphit/VERSION.json +1 -1
  38. package/skills/graphit/cursor/graphit-sql-reference.mdc +2 -2
  39. package/skills/graphit/graphit.mdc +6 -9
  40. package/skills/graphit/references/governance-explained.md +2 -2
  41. package/skills/graphit/references/governance.md +22 -17
  42. package/skills/graphit/references/kb-actions.md +2 -3
  43. package/skills/graphit/references/kb-traversal.md +1 -1
  44. package/skills/graphit/references/operations.md +1 -1
  45. package/skills/graphit/references/runtime.md +5 -1
@@ -221,15 +221,13 @@ You are the rendering layer - format and present every CLI result using markdown
221
221
  | `graphit ds refresh --all --skip-empty` | Refresh non-empty data sources only |
222
222
  | `graphit ds refresh --all --no-wait` | Trigger all refreshes without waiting |
223
223
  | `graphit ds refresh <id> [id2...]` | Refresh one or more data sources by ID |
224
- | `graphit ds update <id> --governed-mode on\|off` | Enable/disable governed mode on a data source |
225
- | `graphit ds update <id> --max-rows N` | Set max rows cap on a data source |
224
+ | `graphit ds update <id> --max-rows N` | Set or clear a data-source row cap |
226
225
  | `graphit query "<sql>" --ds <id>` | Query cached data source (~100ms) |
227
226
  | `graphit query "<sql>" --ds <id> --override-rules RULE1 RULE2` | Query with governance rule overrides |
228
227
  | `graphit query "<sql>" --ds <id> --verbose` | Show expanded SQL and trust tier |
229
- | `graphit query "<sql>" --ds <id> --approve-adhoc` | Approve running an ad-hoc measure query on a governed source |
228
+ | `graphit query "<sql>" --ds <id> --adhoc-reason "<reason>"` | Justify an ad-hoc query when no KB definition fits |
230
229
  | `graphit query "<sql>" --warehouse --connection <id>` | Query live Snowflake (~10s) |
231
- | `graphit governance status` | Show governance mode and conformance stats |
232
- | `graphit governance set <mode>` | Set governance mode (observe/warn/strict) |
230
+ | `graphit governance status` | Show governance conformance stats |
233
231
  | `graphit governance audit` | View recent governance audit events |
234
232
  | `graphit dashboard create --name "..."` | Create dashboard (returns ID) |
235
233
  | `graphit dashboard get-html <id>` | Get current HTML content of a dashboard |
@@ -264,12 +262,11 @@ Use KB references for governed queries:
264
262
  graphit query "SELECT {{dim:INSTALL_MONTH}}, {{metric:CPI}} as cpi FROM MARKETING_UA_DS GROUP BY 1" --ds ds_abc123
265
263
  graphit query "SELECT {{metric:ARPU(DAY=7)}} as arpu FROM MARKETING_UA_DS" --ds ds_abc123 # parameterized
266
264
  graphit query "SELECT * FROM events" --ds ds_123 --override-rules EXCLUDE_RETARGETING # bypass rule
267
- graphit governance status # view mode and conformance
268
- graphit governance set warn # change mode (admin)
269
- graphit ds update <id> --governed-mode on # enable governance on DS
265
+ graphit governance status # view conformance
266
+ graphit ds update <id> --max-rows 10000 # cap result size
270
267
  ```
271
268
 
272
- Reference syntax in `graphit.resolve()` calls works the same way - server expands at query time. Trust tiers: **governed** (KB refs), **verified** (matches KB), **ad-hoc** (inline). On a governed DS, an ad-hoc **measure** query (aggregate / `GROUP BY` without `{{metric:X}}`) is gated: warn mode rejects it (rewrite with `{{metric:X}}`, or ask the user and re-run with `--approve-adhoc`); strict hard-blocks all ad-hoc. Exploration (`SELECT *`, `COUNT(*)`, `DISTINCT`) runs free.
269
+ Reference syntax in `graphit.resolve()` calls works the same way - server expands at query time. Trust tiers: **governed** (KB refs), **verified** (matches KB), **ad-hoc** (inline). On the CLI, an ad-hoc query needs a substantive `--adhoc-reason` when no KB definition fits. An ad-hoc **business measure** (aggregate / `GROUP BY` without `{{metric:X}}`) is hard-blocked when the caller lacks EXPLORE access to any queried scope; a reason never bypasses that access denial.
273
270
 
274
271
  ---
275
272
 
@@ -16,7 +16,7 @@ Governance means every number is computed the team's agreed way and carries an h
16
16
  | verified | Raw SQL whose math matches a KB definition | Amber |
17
17
  | ad_hoc | Inline formula with no KB match | Gray |
18
18
 
19
- Enforcement is server-side and identical on every channel (agent, CLI, dashboard, warehouse); it cannot be weakened from the CLI. The org's governance mode sets how hard rules bite: observe (log only), warn (warn but run), strict (block, override only if the rule and the user's role allow). If a query was blocked or asked for a justification, that is the ad-hoc gate: it used no KB reference and matched no definition, so Graphit wants you to either rewrite it with a metric or dimension (creating one if it is missing) or state honestly why a raw run is warranted. The reason is recorded in the audit log.
19
+ Enforcement is server-side and identical on every channel (agent, CLI, dashboard, warehouse); it cannot be weakened from the CLI. An ad-hoc business measure requires EXPLORE access across every table it touches. A rule override additionally requires EXPLORE, the rule policy, and the user's role to allow it. If a query was blocked or asked for a justification, that is the ad-hoc gate: it used no KB reference and matched no definition, so Graphit wants you to either rewrite it with a metric or dimension (creating one if it is missing) or state honestly why a raw run is warranted. The reason is recorded in the audit log; it never bypasses missing access or a disallowed rule override.
20
20
 
21
21
  **2. Auditing, after the fact (Proactive Insights).** Separately, Graphit continuously scans everything the team built - KB definitions, dashboard graphs, the queries that actually ran - and turns each governance gap into a ranked Fix card routed to an owner. Three principles: a card shows only when Graphit is confident it is real (right or silent), one root cause is one card even if it spans many surfaces, and each is routed to an owner (the resource's owner, else its domain owner, else org admins). The ten card types:
22
22
 
@@ -33,7 +33,7 @@ Enforcement is server-side and identical on every channel (agent, CLI, dashboard
33
33
  | Unenforced rule | A rule that structurally cannot act on anything |
34
34
  | Missing owner | Assets with nobody responsible for them |
35
35
 
36
- Fix is a guided one-click resolution: it drafts and previews the exact change, you apply it (analyst seat, same validation as a manual edit), then Graphit re-checks and clears the card. This queue lives only in the **web app's Governance page** - the CLI cannot list or fix Insights cards (`governance status` and `governance audit` report mode and conformance only). When a user asks about a finding, explain what it means and point them to the Governance page to Fix it.
36
+ Fix is a guided one-click resolution: it drafts and previews the exact change, you apply it (analyst seat, same validation as a manual edit), then Graphit re-checks and clears the card. This queue lives only in the **web app's Governance page** - the CLI cannot list or fix Insights cards (`governance status` and `governance audit` report conformance only). When a user asks about a finding, explain what it means and point them to the Governance page to Fix it.
37
37
 
38
38
  ## Relaying it
39
39
 
@@ -1,8 +1,8 @@
1
1
  # Query Governance
2
2
 
3
- Load this when writing or validating a governed query, working trust tiers, or working the ad-hoc frontier (deciding whether an ad-hoc query is allowed, and justifying it when it is).
3
+ Load this when writing or validating a governed query, working trust tiers, declaring a conditionally-enforced rule, or working the ad-hoc frontier.
4
4
 
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.
5
+ Governance is enforced server-side in the QueryGateway, the same across every channel. You CANNOT weaken it from the CLI: an ad-hoc business measure requires EXPLORE access across every queried scope, and rule overrides must also satisfy EXPLORE.
6
6
 
7
7
  ## Reference syntax
8
8
 
@@ -27,7 +27,7 @@ Some metrics (for example ARPU, ROAS, RETENTION) carry required parameters and c
27
27
 
28
28
  ## Trust tiers
29
29
 
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.
30
+ Every result is stamped with a tier, shown to the user as a badge on graphs and canvas entities - your honest signal of how trustworthy the number is.
31
31
 
32
32
  | Tier | Meaning | Badge |
33
33
  |------|---------|-------|
@@ -39,12 +39,12 @@ Prefer the governed tier. The server may upgrade matching raw SQL to verified, b
39
39
 
40
40
  ## The ad-hoc gate
41
41
 
42
- This is the hard frontier, enforced server-side. The rules below are what the QueryGateway does, not a suggestion you can soften.
42
+ This is the hard frontier - the rules below are what the QueryGateway does.
43
43
 
44
- A query lands at the `ad_hoc` tier when it uses no `{{metric:NAME}}` / `{{dim:NAME}}` reference and its raw expressions match no KB definition. On the CLI (`graphit query`, cached `--ds` or `--warehouse`), **every** ad-hoc query must be justified - business measures (an aggregate or `GROUP BY`) AND plain exploration alike (`SELECT *`, raw column selects, `COUNT(*)` peeks, `DISTINCT` value lists). Governed and verified results are exempt.
44
+ A query lands at the `ad_hoc` tier when it uses no `{{metric:NAME}}` / `{{dim:NAME}}` reference and its raw expressions match no KB definition. On the CLI (`graphit query`, cached `--ds` or `--warehouse`), **every** ad-hoc query must be justified - business measures (an aggregate or `GROUP BY`) and plain exploration alike (`SELECT *`, `COUNT(*)`, `DISTINCT` peeks). Governed and verified results are exempt.
45
45
 
46
- - **observe / warn.** The ad-hoc query is withheld and asks for a justification. Preferred path first: rewrite with `{{metric:NAME}}` / `{{dim:NAME}}` references - genuinely search the KB (`graphit kb explore`, `graphit kb list metric`, `graphit kb list dimension`), and if the metric or dimension you need does not exist, CREATE it first. A filter's value list belongs in a `{{dim:NAME}}`, not a raw `SELECT DISTINCT` peek. Only if nothing fits and the user needs the raw run, pass `--adhoc-reason "<text>"` stating what you searched, what you found, and why it does not fit. When you already know the query is ad-hoc, pass it on the first call to skip the round-trip. A trivial or empty reason is rejected server-side, and every justification is recorded in the audit log - so it must be honest, not a rationalization.
47
- - **strict.** CRITICAL: every `ad_hoc`-tier query on a governed DS is a HARD BLOCK. Rewrite with references or stop. `--adhoc-reason` NEVER bypasses strict and does not weaken it; do not offer it in strict mode.
46
+ - **EXPLORE access is a hard prerequisite for ad-hoc business measures.** If the user lacks EXPLORE access to any queried table scope, the server blocks that measure. An ad-hoc reason cannot bypass that denial.
47
+ - **Justification floor.** The ad-hoc query is withheld and asks for a justification. Preferred path first: rewrite with `{{metric:NAME}}` / `{{dim:NAME}}` references - genuinely search the KB (`graphit kb explore`, `graphit kb list metric`, `graphit kb list dimension`), and if the metric or dimension you need does not exist, CREATE it first. A filter's value list belongs in a `{{dim:NAME}}`, not a raw `SELECT DISTINCT` peek. Only if nothing fits and the user needs the raw run, pass `--adhoc-reason "<text>"` stating what you searched, what you found, and why it does not fit - pass it on the first call when you already know the query is ad-hoc. A trivial or empty reason is rejected server-side and recorded in the audit log, so it must be honest.
48
48
 
49
49
  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.
50
50
 
@@ -60,36 +60,41 @@ Rules with typed constraints are enforced automatically, rewriting the SQL befor
60
60
  | `required_filter` | Validates a column appears in WHERE |
61
61
  | `required_aggregation` | Validates GROUP BY includes a column |
62
62
 
63
- 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.
63
+ User-context variables (`${user.team_id}`, `${user.email}`) resolve server-side for row-level security. Override a rule only when the user explicitly asks; the server honors it only if the user holds EXPLORE on every queried scope. A rule that masks a column (a `forbidden_column` constraint) can never be overridden. Every override is logged. Pass several names to override more than one.
64
64
 
65
65
  ```bash
66
66
  graphit query "SELECT * FROM EVENTS" --ds EVENTS --override-rules EXCLUDE_RETARGETING
67
67
  ```
68
68
 
69
- ## Per-DS settings and governance mode
69
+ ## Conditionally-enforced rules
70
70
 
71
- Governed mode and the row cap are set per data source; enabling governed mode lets `strict` hard-block ad-hoc queries on that DS (the CLI justification floor above applies regardless of this setting). 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.
71
+ A rule's mode is Advisory (guidance), Always (every query), or **Conditional** - fires only when its plain-language body (the condition) holds for the query you wrote. No server classifier decides that; you do. Read a table's rules first (`graphit kb explore table <name>`), judge your query against each conditional rule's body, and declare it up front:
72
72
 
73
- ```bash
74
- graphit governance set --mode warn
75
- ```
73
+ - `--apply-conditional RULE` - enforce it for this query.
74
+ - `--skip-conditional RULE:"reason"` - skip it; a reason is required and audit-logged.
75
+
76
+ Declare up front so a clean query never stalls. An undeclared conditional returns a retryable prompt naming each unresolved rule and its condition - read it, decide, re-run. A saved tile stores your decision and replays it every refresh.
77
+
78
+ ## Data-source row caps
79
+
80
+ An admin can set a `max_rows` cap per data source. A cap limits the result set; it does not change authorization, trust-tier classification, or the ad-hoc justification floor.
76
81
 
77
82
  ## Presenting governance results
78
83
 
79
84
  The user CANNOT see raw CLI output. Render every result as markdown.
80
85
 
81
- **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.
86
+ **After a governed query**, append a provenance footer so the trust signal travels with the number: tier, KB references used, row cap if applied. Because a governed query may be rewritten before it runs, read `provenance.injection_summary` and report which rules changed it and how (each rule, its outcome, and why), not just a count. An ungoverned query reports no rules applied. For an ad-hoc result, state the tier honestly and offer the governed `{{metric:NAME}}` / `{{dim:NAME}}` rewrite.
82
87
 
83
88
  ~~~
84
- **Trust tier:** governed - governed DS, 2 KB refs, 1 rule enforced (**EXCLUDE_INTERNAL**), max rows 10000
89
+ **Trust tier:** governed - 2 KB refs, 1 rule enforced (**EXCLUDE_INTERNAL**), max rows 10000
85
90
  ~~~
86
91
 
87
- **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>`.
92
+ **After `graphit governance status`**, show the 7-day conformance counts (governed / verified / ad-hoc / total) as a small markdown table.
88
93
 
89
94
  **When a gate or rule blocks a query**, explain it and the path forward, no raw dump:
90
95
 
91
96
  ~~~
92
97
  **Blocked by governance.**
93
98
 
94
- 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.
99
+ Rule **EXCLUDE_ORGANIC** is enforced; overriding it requires EXPLORE access on every queried scope. Rewrite the query to include the required filter, or ask an admin for EXPLORE access.
95
100
  ~~~
@@ -29,7 +29,7 @@ Present the plan, then stop. Do not create until the user approves.
29
29
  |---|---|
30
30
  | Metric | `graphit kb create metric --name X --sql "<expr>" --table T` (optional `--topics "A,B"`, `--default-dimensions "D1,D2"`, `--parameters`/`--parameters-file` for templates, `--skip-validate`) |
31
31
  | Dimension | `graphit kb create dimension --name X --expr "<expr>" --table T` (type auto-inferred; override with `--type` / `--output-type`; `--skip-validate`) |
32
- | Rule | `graphit kb create rule --name X --sql "<text>" --table T` (optional `--constraint`, `--override-policy`, `--apply-on`, `--topics`, `--skip-validate`) |
32
+ | Rule | `graphit kb create rule --name X --sql "<text>" --table T` (optional `--constraint`, `--apply-on`, `--topics`, `--skip-validate`) |
33
33
  | Synonym | `graphit kb create synonym --term X --canonical Y --type metric` |
34
34
  | Domain | `graphit kb create domain --name X` (optional `--color "#4DB6AC"`) |
35
35
  | Topic | `graphit kb create topic --name X` |
@@ -40,12 +40,11 @@ Present the plan, then stop. Do not create until the user approves.
40
40
  A plain rule is documentation. To make it enforced server-side at query time, pass typed constraints on `graphit kb create rule` / `graphit kb update rule`:
41
41
 
42
42
  - `--constraint <spec...>` - one or more typed constraints, each written `type:value`. Types: `required_where:"<predicate>"`, `forbidden_column:<col>`, `required_filter:<col>`, `required_aggregation:<col>`, `value_restriction:<col>:<in|not_in>:<v1,v2>`. On update the supplied list REPLACES the rule's existing constraints.
43
- - `--override-policy <policy>` - who may bypass the rule with `--override-rules`: `anyone`, `analyst_only`, `admin_only`, or `never`. Create defaults to `anyone`.
44
43
 
45
44
  What each constraint type does at query time, plus the override flow, lives in `governance.md`. Example: a rule that always scopes verified purchases -
46
45
 
47
46
  ```bash
48
- graphit kb create rule --name FILTER_VERIFIED_PURCHASES --sql "Only count verified purchases" --table ORDERS --constraint required_where:"is_verified = true" --override-policy analyst_only
47
+ graphit kb create rule --name FILTER_VERIFIED_PURCHASES --sql "Only count verified purchases" --table ORDERS --constraint required_where:"is_verified = true"
49
48
  ```
50
49
 
51
50
  ## Rule Targeting - what a rule governs
@@ -79,7 +79,7 @@ Adapt columns per type: dimensions include semantic type, rules include constrai
79
79
  | **Default dims** | MEDIA_SOURCE, CAMPAIGN_NAME |
80
80
  ~~~
81
81
 
82
- Adapt fields per type. Rules: content, constraints, apply-on, override policy, plus `enforced` (Enforced = has constraints / applied to every query; Suggested = body-only / guides the AI) and `governs` (the tables it covers whole + any narrowed metric/dimension targets). Metrics and dimensions carry `governed_by` - the rules governing that asset, each tagged `enforced` and a `scope_tag` (`whole table` vs `this metric`/`this dimension`) - so reading a metric also shows what rules apply to it. Dimensions also: expression, semantic type, output type.
82
+ Adapt fields per type. Rules: content, constraints, apply-on, plus `enforced` (Enforced = has constraints / applied to every query; Suggested = body-only / guides the AI) and `governs` (the tables it covers whole + any narrowed metric/dimension targets). Metrics and dimensions carry `governed_by` - the rules governing that asset, each tagged `enforced` and a `scope_tag` (`whole table` vs `this metric`/`this dimension`) - so reading a metric also shows what rules apply to it. Dimensions also: expression, semantic type, output type.
83
83
 
84
84
  **After `graphit kb search`** - result count + table with type column:
85
85
 
@@ -58,7 +58,7 @@ The CLI enforces the same permission model as the platform. Three codes:
58
58
  |---|---|---|
59
59
  | 403 | Analyst seat or admin role required | Most commands need an Analyst seat. Viewer-seat users can run `graphit auth` only; everything else is blocked, so they use the platform UI. Connector create and delete also need an org owner or admin role, so a non-admin analyst gets 403 there. |
60
60
  | 404 | Not found, or no access | Returned for dashboards the user cannot reach (private ones owned by others, team dashboards they are not on). The API does not distinguish "does not exist" from "you cannot access it", to prevent ID enumeration. Do not assume the dashboard is gone. |
61
- | 423 | Shared dashboard needs an active editing session | Shared-dashboard mutations need the user to open the dashboard on the platform and click Edit first; the CLI cannot acquire the session. See the shared-dashboard constraint in the router. |
61
+ | 423 | Shared dashboard needs an active editing session | Catch one from the CLI: `graphit dashboard edit <id>` acquires the session and starts a draft; make the edits, then `graphit dashboard publish <id>` to go live (or `graphit dashboard release <id> --yes` to abandon). 409 = someone else is editing; 423 = locked; 403 = view-only. Private dashboards need no session. |
62
62
 
63
63
  For the exact remediation flags on the failed command, run it with `--help`.
64
64
 
@@ -31,6 +31,8 @@ const result = await graphit.resolve({
31
31
  - `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
32
  - `result.data` is an array of row objects you render however you want.
33
33
 
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.
35
+
34
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.
35
37
 
36
38
  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.
@@ -113,13 +115,15 @@ A resolve query that follows these shapes serves from a semantic cache in roughl
113
115
  - `CURRENT_DATE`-relative predicates.
114
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).
115
117
 
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.
119
+
116
120
  ## Rate-limit budget
117
121
 
118
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:
119
123
 
120
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.
121
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.
122
- - **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. When one result serves several graphs like this, anchor every graph it feeds with `targetEntityIds` (keep `target` on the primary) so each graph's details panel shows the live filtered query, not stale base SQL. Canonical KPI-row example: `kpi.md`.
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`.
123
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.
124
128
  - **No polling.** NEVER use `setInterval(refresh, ...)`. Data sources update on their own schedule; a polling dashboard burns the entire budget.
125
129