@graphit/cli 0.2.379 → 0.2.384

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 (42) 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/bin/graphit +1 -1
  5. package/bin/graphit.ps1 +1 -1
  6. package/dist/commands/ds/sql-history.d.ts +29 -0
  7. package/dist/commands/ds/sql-history.js +69 -0
  8. package/dist/commands/ds/sql-history.js.map +1 -0
  9. package/dist/commands/ds.js +3 -0
  10. package/dist/commands/ds.js.map +1 -1
  11. package/dist/commands/kb-batch.js +1 -1
  12. package/dist/commands/kb-batch.js.map +1 -1
  13. package/dist/commands/kb.js +2 -21
  14. package/dist/commands/kb.js.map +1 -1
  15. package/dist/skill-guard.js +0 -2
  16. package/dist/skill-guard.js.map +1 -1
  17. package/hooks/hooks.json +12 -1
  18. package/package.json +1 -1
  19. package/scripts/fixtures/kb-cutover-project293.json +13 -0
  20. package/scripts/plugin-status/claude-install.mjs +215 -0
  21. package/scripts/plugin-status/cloud-refresh.mjs +97 -0
  22. package/scripts/plugin-status/common.mjs +107 -0
  23. package/scripts/plugin-status/hook-io.mjs +200 -0
  24. package/scripts/plugin-status.mjs +68 -505
  25. package/scripts/show-query-result.mjs +23 -4
  26. package/scripts/sync-plugin-marketplace.sh +18 -0
  27. package/scripts/verb-policy-source.json +49 -30
  28. package/scripts/verify-kb-cutover.mjs +134 -0
  29. package/skills/graphit/SKILL.md +7 -7
  30. package/skills/graphit/VERSION.json +1 -1
  31. package/skills/graphit/references/governance.md +2 -2
  32. package/skills/graphit/references/kb-actions.md +5 -5
  33. package/skills/graphit/references/kb-discovery.md +1 -1
  34. package/skills/graphit/references/metric-families.md +1 -1
  35. package/skills/graphit/references/onboarding.md +1 -1
  36. package/skills/graphit/references/page-load.md +38 -0
  37. package/skills/graphit/references/runtime.md +5 -5
  38. package/skills/graphit/references/semantic-authoring.md +5 -5
  39. package/skills/graphit/references/share.md +4 -4
  40. package/skills/graphit-build/SKILL.md +1 -1
  41. package/skills/graphit-explore/SKILL.md +1 -1
  42. package/skills/graphit-share/SKILL.md +5 -5
@@ -31,6 +31,27 @@ function isGraphitQuery(command) {
31
31
  return /(?:graphit|index\.js)\s+query\b/.test(command);
32
32
  }
33
33
 
34
+ // Issue #1025: a query whose stdout is piped (`|`) or sent to a file (`>`) never
35
+ // reaches this Bash call's output, so any JSON there belongs to another command.
36
+ // Formats only when at least one query invocation writes to the call's stdout.
37
+ // `2>` / `2>&1` redirect stderr only and do not count.
38
+ function queryReachesStdout(command) {
39
+ const invocation = /(?:graphit|index\.js)\s+query\b/g;
40
+ for (const match of command.matchAll(invocation)) {
41
+ const tail = command.slice(match.index).split(/\n|;|&&|\|\||(?<![|&])&(?![&>])/)[0];
42
+ const withoutStderr = tail.replace(/\d?>&\d|2>>?\s*\S+/g, "");
43
+ if (!/(?<!\|)\|(?!\|)|>/.test(withoutStderr)) return true;
44
+ }
45
+ return false;
46
+ }
47
+
48
+ // Issue #1025: every CLI command prints JSON, and some carry `row_count`
49
+ // (`ds create`, `ds re-upload`). Only a query result has a `rows` array together
50
+ // with its `columns` or `provenance`.
51
+ function isQueryResult(parsed) {
52
+ return Array.isArray(parsed.rows) && ("columns" in parsed || "provenance" in parsed);
53
+ }
54
+
34
55
  // The CLI prints verbose SQL before and a provenance footer after the JSON, so
35
56
  // we extract the first complete top-level JSON object (string-aware brace match).
36
57
  function extractFirstJsonObject(text) {
@@ -166,7 +187,7 @@ function main() {
166
187
  if (payload.tool_name !== "Bash") return;
167
188
 
168
189
  const command = payload.tool_input?.command ?? "";
169
- if (!isGraphitQuery(command)) return;
190
+ if (!isGraphitQuery(command) || !queryReachesStdout(command)) return;
170
191
 
171
192
  // tool_response may be an object ({stdout,stderr,...}) or a raw string.
172
193
  const resp = payload.tool_response;
@@ -188,9 +209,7 @@ function main() {
188
209
  emit(`### Graphit query failed\n\n\`${clean(parsed.error)}\``);
189
210
  return;
190
211
  }
191
- if (!("rows" in parsed) && !("data" in parsed) && !("row_count" in parsed)) {
192
- return;
193
- }
212
+ if (!isQueryResult(parsed)) return;
194
213
  emit(buildMarkdown(parsed));
195
214
  }
196
215
 
@@ -20,6 +20,10 @@ sync_marketplace() {
20
20
  # Project #298: the copied hook manifest must not reference absent scripts.
21
21
  cp cli/scripts/plugin-status.mjs cli/scripts/block-legacy-setup.mjs \
22
22
  cli/scripts/show-query-result.mjs "$target/scripts/"
23
+ # Feature #1042: plugin-status.mjs imports its modules from this folder; verify
24
+ # follows each hook script's relative imports, so a missing module fails there.
25
+ mkdir -p "$target/scripts/plugin-status"
26
+ rsync -a --delete cli/scripts/plugin-status/ "$target/scripts/plugin-status/"
23
27
 
24
28
  # Project #246: the public plugin ships wrappers that invoke the current CLI.
25
29
  mkdir -p "$target/bin"
@@ -98,6 +102,20 @@ for (const groups of Object.values(config.hooks)) {
98
102
  }
99
103
  }
100
104
  }
105
+ // Feature #1042: a hook script's relative imports ship with it, so check them too.
106
+ const importPattern = /(?:\bfrom\s+|\bimport\s*\(\s*|\bimport\s+)["'](\.{1,2}\/[^"']+)["']/g;
107
+ const queue = [...files];
108
+ while (queue.length > 0) {
109
+ const file = queue.shift();
110
+ if (!file.endsWith(".mjs") || !fs.existsSync(path.join("cli", file))) continue;
111
+ for (const match of fs.readFileSync(path.join("cli", file), "utf8").matchAll(importPattern)) {
112
+ const dependency = path.posix.normalize(path.posix.join(path.posix.dirname(file), match[1]));
113
+ if (!files.has(dependency)) {
114
+ files.add(dependency);
115
+ queue.push(dependency);
116
+ }
117
+ }
118
+ }
101
119
  let valid = true;
102
120
  for (const file of files) {
103
121
  const destination = path.join(target, file);
@@ -300,22 +300,6 @@
300
300
  "requires_approval": true,
301
301
  "silent_retry_exempt": false
302
302
  },
303
- "kb verify": {
304
- "surface": "both",
305
- "noun": "kb_write",
306
- "is_read_only": false,
307
- "mutation_class": "kb",
308
- "requires_approval": true,
309
- "silent_retry_exempt": false
310
- },
311
- "kb unverify": {
312
- "surface": "both",
313
- "noun": "kb_write",
314
- "is_read_only": false,
315
- "mutation_class": "kb",
316
- "requires_approval": true,
317
- "silent_retry_exempt": false
318
- },
319
303
  "kb column-visibility": {
320
304
  "surface": "both",
321
305
  "noun": "kb_write",
@@ -440,6 +424,13 @@
440
424
  "requires_approval": false,
441
425
  "silent_retry_exempt": false
442
426
  },
427
+ "ds sql-history": {
428
+ "surface": "both",
429
+ "is_read_only": true,
430
+ "mutation_class": "none",
431
+ "requires_approval": false,
432
+ "silent_retry_exempt": false
433
+ },
443
434
  "ds usage": {
444
435
  "surface": "both",
445
436
  "is_read_only": true,
@@ -514,32 +505,60 @@
514
505
  "reason": "Not a working verb on either surface, and there is no UI action behind it (Issue #962). A source has no home of its own: it lives in the group of the semantic model bound to it, so `kb update semantic-model` with a new group is the move. The CLI registers `ds move` only to say so."
515
506
  },
516
507
  "dashboard folder spaces": {
517
- "surface": "both", "noun": "dashboard", "is_read_only": true,
518
- "mutation_class": "none", "requires_approval": false, "silent_retry_exempt": false
508
+ "surface": "both",
509
+ "noun": "dashboard",
510
+ "is_read_only": true,
511
+ "mutation_class": "none",
512
+ "requires_approval": false,
513
+ "silent_retry_exempt": false
519
514
  },
520
515
  "dashboard folder list": {
521
- "surface": "both", "noun": "dashboard", "is_read_only": true,
522
- "mutation_class": "none", "requires_approval": false, "silent_retry_exempt": false
516
+ "surface": "both",
517
+ "noun": "dashboard",
518
+ "is_read_only": true,
519
+ "mutation_class": "none",
520
+ "requires_approval": false,
521
+ "silent_retry_exempt": false
523
522
  },
524
523
  "dashboard folder get": {
525
- "surface": "both", "noun": "dashboard", "is_read_only": true,
526
- "mutation_class": "none", "requires_approval": false, "silent_retry_exempt": false
524
+ "surface": "both",
525
+ "noun": "dashboard",
526
+ "is_read_only": true,
527
+ "mutation_class": "none",
528
+ "requires_approval": false,
529
+ "silent_retry_exempt": false
527
530
  },
528
531
  "dashboard folder create": {
529
- "surface": "both", "noun": "dashboard", "is_read_only": false,
530
- "mutation_class": "dashboard", "requires_approval": true, "silent_retry_exempt": true
532
+ "surface": "both",
533
+ "noun": "dashboard",
534
+ "is_read_only": false,
535
+ "mutation_class": "dashboard",
536
+ "requires_approval": true,
537
+ "silent_retry_exempt": true
531
538
  },
532
539
  "dashboard folder update": {
533
- "surface": "both", "noun": "dashboard", "is_read_only": false,
534
- "mutation_class": "dashboard", "requires_approval": true, "silent_retry_exempt": true
540
+ "surface": "both",
541
+ "noun": "dashboard",
542
+ "is_read_only": false,
543
+ "mutation_class": "dashboard",
544
+ "requires_approval": true,
545
+ "silent_retry_exempt": true
535
546
  },
536
547
  "dashboard folder delete": {
537
- "surface": "both", "noun": "dashboard", "is_read_only": false,
538
- "mutation_class": "dashboard", "requires_approval": true, "silent_retry_exempt": true
548
+ "surface": "both",
549
+ "noun": "dashboard",
550
+ "is_read_only": false,
551
+ "mutation_class": "dashboard",
552
+ "requires_approval": true,
553
+ "silent_retry_exempt": true
539
554
  },
540
555
  "dashboard move": {
541
- "surface": "both", "noun": "dashboard", "is_read_only": false,
542
- "mutation_class": "dashboard", "requires_approval": true, "silent_retry_exempt": true
556
+ "surface": "both",
557
+ "noun": "dashboard",
558
+ "is_read_only": false,
559
+ "mutation_class": "dashboard",
560
+ "requires_approval": true,
561
+ "silent_retry_exempt": true
543
562
  },
544
563
  "dashboard list": {
545
564
  "surface": "both",
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ /** Bounded local fixture proof. Uses the normal CLI client/auth without modifying credentials. */
3
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
4
+ import { randomBytes } from 'node:crypto';
5
+ import { dirname, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ export const FIXTURE_ORG = 'zz-test-293-kb-cutover';
9
+ const BASE = '/api/v1/cli/kb';
10
+ const RETIRED_KEYS = new Set(['verified', 'verified_by', 'verified_at', 'unverified']);
11
+ const safeId = value => typeof value === 'string' && /^[a-zA-Z0-9_-]{1,128}$/.test(value);
12
+ const statusCode = error => Number.isInteger(error?.status) ? error.status : null;
13
+ const entityOf = response => {
14
+ if (response?.entity && typeof response.entity === 'object') return response.entity;
15
+ if (response?.rule && typeof response.rule === 'object') return response.rule;
16
+ return response;
17
+ };
18
+ function hasRetiredMetadata(entity) {
19
+ return Object.keys(entity ?? {}).some(k => RETIRED_KEYS.has(k)) ||
20
+ Object.keys(entity?.meta?.graphit ?? {}).some(k => RETIRED_KEYS.has(k));
21
+ }
22
+ function blocked(reason) { return {scope:'kb_authoring_and_retired_client_contract',status:'blocked',reason,created:[],checks:[],cleanup:[]}; }
23
+
24
+ export async function runKBVerification({api, orgId, apiBaseUrl, runId = randomBytes(5).toString('hex')}) {
25
+ if (orgId !== FIXTURE_ORG) return blocked('undesignated_fixture_org');
26
+ let url;
27
+ try { url = new URL(apiBaseUrl); } catch { return blocked('invalid_api_url'); }
28
+ if (url.protocol !== 'http:' || !['localhost','127.0.0.1','[::1]'].includes(url.hostname) || url.username || url.password) return blocked('local_api_required');
29
+ if (!/^[a-z0-9]{1,16}$/.test(runId)) return blocked('invalid_run_id');
30
+
31
+ const report = {...blocked(null),status:'failed',org_id:orgId,run_id:runId};
32
+ report.not_run = [
33
+ {check:'cached_query_and_constraint_enforcement',reason:'No approved source fixture or cloud storage/KMS path supplied.'},
34
+ {check:'source_activation_and_breaking_schema_acceptance',reason:'No approved source fixture supplied.'},
35
+ {check:'scoped_non_admin_weakening_and_delete',reason:'Current CLI identity only; no separate member session supplied.'},
36
+ {check:'physical_tombstone_inspection',reason:'Public CRUD proves live absence only; storage inspection belongs to the cut runner.'},
37
+ ];
38
+ const owned = [];
39
+ report.unconfirmed_writes = [];
40
+ try {
41
+ const status = await api.get('/api/v1/cli/status');
42
+ if (status?.identity?.org_id !== orgId) return blocked('authenticated_org_mismatch');
43
+ if (!['owner','admin'].includes(status?.identity?.org_role)) return blocked('fixture_admin_required');
44
+ // A clean run only. Names with our prefix are not ownership evidence.
45
+ for (const noun of ['group','semantic-model','metric','rules','template']) {
46
+ const page = await api.get(`${BASE}/${noun}`);
47
+ if (!Array.isArray(page?.items) || page.total !== 0 || page.items.length || page.truncated || page.next_cursor) return blocked('fixture_not_empty');
48
+ }
49
+ report.checks.push({check:'authenticated_empty_visible_fixture',status:'passed'});
50
+ const fixtures = JSON.parse(await readFile(new URL('./fixtures/kb-cutover-project293.json',import.meta.url),'utf8'));
51
+ const names = Object.fromEntries(['group','semantic-model','metric'].map(noun => [noun,`zz_test_293_${noun.replaceAll('-','_')}_${runId}`]));
52
+ names.rule = `ZZ_TEST_293_RULE_${runId.toUpperCase()}`;
53
+ for (const noun of ['group','semantic-model','metric','rule']) {
54
+ const definition = structuredClone(fixtures[noun]);
55
+ definition.name = names[noun];
56
+ if (noun === 'metric' || noun === 'semantic-model') definition.group = names.group;
57
+ if (noun === 'rule') definition.apply_on = [{type:'model',name:names['semantic-model']}];
58
+ const body = noun === 'rule' ? definition : {definition};
59
+ let response;
60
+ try { response=await api.post(`${BASE}/${noun}`,body); }
61
+ catch(error) {
62
+ if(statusCode(error)===null || statusCode(error)>=500) report.unconfirmed_writes.push({noun,name:names[noun]});
63
+ throw error;
64
+ }
65
+ if(response?.detail?.applied === true && safeId(response.detail.doc_id)) {
66
+ const artifact={noun,name:names[noun],id:response.detail.doc_id};
67
+ owned.push(artifact);report.created.push({...artifact});
68
+ throw {status:202};
69
+ }
70
+ const created = entityOf(response);
71
+ if (!safeId(created?.id) || created.name !== names[noun]) throw {status:null};
72
+ const artifact = {noun,name:created.name,id:created.id};
73
+ owned.push(artifact); report.created.push({...artifact});
74
+ if (hasRetiredMetadata(created)) throw {status:null};
75
+ const readPath = noun === 'rule' ? `${BASE}/rules/${encodeURIComponent(created.id)}` : `${BASE}/${noun}/${encodeURIComponent(created.name)}`;
76
+ const actual = entityOf(await api.get(readPath));
77
+ if (actual?.id !== created.id || actual?.name !== created.name || hasRetiredMetadata(actual)) throw {status:null};
78
+ report.checks.push({check:`save_and_read_${noun}`,status:'passed',id:created.id});
79
+ }
80
+ const refusalCases = [
81
+ {check:'old_cli_unverified_false',path:`${BASE}/metric`,body:{definition:{...fixtures.metric,name:`zz_test_293_rejected_${runId}`,group:names.group},unverified:false},codes:[422]},
82
+ {check:'old_nested_verification',path:`${BASE}/metric`,body:{definition:{...fixtures.metric,name:`zz_test_293_rejected_${runId}`,group:names.group,meta:{graphit:{verified:false}}}},codes:[400,422]},
83
+ {check:'manual_verification_action',path:`${BASE}/metric/${names.metric}/verify`,body:{verified:true},codes:[404,405]},
84
+ ];
85
+ for (const scenario of refusalCases) {
86
+ let code = null;
87
+ try {
88
+ const accepted = entityOf(await api.post(scenario.path,scenario.body));
89
+ if (scenario.body.definition && safeId(accepted?.id) && accepted.name === scenario.body.definition.name) {
90
+ const artifact={noun:'metric',name:accepted.name,id:accepted.id};
91
+ owned.push(artifact);report.created.push({...artifact});
92
+ }
93
+ } catch (error) { code=statusCode(error); }
94
+ report.checks.push({check:scenario.check,status:scenario.codes.includes(code)?'passed':'failed',http_status:code});
95
+ if (!scenario.codes.includes(code)) throw {status:code};
96
+ }
97
+ report.status='passed';delete report.reason;
98
+ } catch (error) {
99
+ report.status='failed';report.reason='scenario_failed';report.http_status=statusCode(error);
100
+ } finally {
101
+ for (const artifact of owned.reverse()) {
102
+ const readPath = artifact.noun === 'rule' ? `${BASE}/rules/${encodeURIComponent(artifact.id)}` : `${BASE}/${artifact.noun}/${encodeURIComponent(artifact.name)}`;
103
+ try {
104
+ const current = entityOf(await api.get(readPath));
105
+ if (current?.id !== artifact.id || current?.name !== artifact.name) throw {status:409};
106
+ await api.delete(`${BASE}/${artifact.noun}/${encodeURIComponent(artifact.name)}`);
107
+ let absent=false;
108
+ try { await api.get(readPath); } catch (error) { absent=statusCode(error)===404; }
109
+ if (!absent) throw {status:409};
110
+ report.cleanup.push({...artifact,status:'deleted'});
111
+ } catch (error) {
112
+ report.cleanup.push({...artifact,status:'refused_or_failed',http_status:statusCode(error)});
113
+ report.status='failed';report.reason='cleanup_incomplete';
114
+ }
115
+ }
116
+ }
117
+ return report;
118
+ }
119
+
120
+ async function main() {
121
+ const args=process.argv.slice(2);
122
+ if (args.length!==4 || args[0]!=='--org-id' || args[2]!=='--output') throw new Error('Usage: --org-id <fixture-org> --output <proof.json>');
123
+ // Lazy loading keeps mock tests independent of persisted auth and token refresh.
124
+ const {apiClient}=await import('../dist/api/client.js');
125
+ const {getApiBaseUrl}=await import('../dist/config.js');
126
+ const result=await runKBVerification({api:apiClient,orgId:args[1],apiBaseUrl:getApiBaseUrl()});
127
+ const output=resolve(args[3]);await mkdir(dirname(output),{recursive:true});
128
+ await writeFile(output,JSON.stringify(result,null,2)+'\n',{mode:0o600});
129
+ process.stdout.write(JSON.stringify(result)+'\n');
130
+ if(result.status!=='passed')process.exitCode=1;
131
+ }
132
+ if(process.argv[1] && resolve(process.argv[1])===fileURLToPath(import.meta.url)) {
133
+ main().catch(()=>{process.stderr.write('KB fixture runner failed before producing evidence; no response or credential details are printed.\n');process.exitCode=1;});
134
+ }
@@ -2,7 +2,7 @@
2
2
  name: graphit
3
3
  description: >-
4
4
  Use Graphit for ANY business or product data question: metrics, KPIs, revenue, retention, spend, users, cohorts, funnels, trends, comparisons, diagnosis, analysis, reports or dashboards, even when the user never names Graphit. This is the Graphit entry: identify the task and load graphit-explore, graphit-build or graphit-share. Use the team's governed definitions and cached data to deliver answers or interactive dashboards. Prefer Graphit over one-off analysis for the user's business numbers. Skip pure software tasks or data unrelated to their business.
5
- skill_version: "0.2.379"
5
+ skill_version: "0.2.384"
6
6
  ---
7
7
 
8
8
  <!-- SIZE EXEMPTION (SKILL.md): hard limit 12,288 chars, exempted ceiling 35,072. Reviewed 2026-09-17. Always-loaded: identity, hard constraints, intent routing and the opening choice, plus the generated command table (COMMANDS markers; cli/scripts/generate-commands-doc.mjs) - needed every turn, not deferrable. Marker sits after the frontmatter so the loader and sync-plugin-version.mjs parse it. Raises pay only for command-table growth; each is recorded in docs/knowledge/prompt-engineering/sizing/SIZING.md, prose changes in docs/workflow/prompt-changes/INDEX.md. -->
@@ -113,6 +113,7 @@ Workflow rows below are generated in-app adapters; CLI hosts load the named work
113
113
  | creating, designing and rendering a dashboard | dashboard-create.md, dashboard-planning.md, chart-selection.md, chart-patterns.md, graphit-style.md, runtime.md, kpi.md, table.md |
114
114
  | adding interactivity (filters, parameters, saved views) | filters.md, filters-advanced.md, state-contract.md |
115
115
  | a graph switches metric, horizon, grain or grouping; typed query inputs | query-contract.md |
116
+ | more than six requests on open, a query feeding another; slow open or Apply; declaring readiness | page-load.md |
116
117
  | reusing a chart across dashboards as a template, or expanding one on a host | templates.md |
117
118
  | building a slide deck | presentations.md |
118
119
  | scheduling, changing, sending or troubleshooting a scheduled report (email/Slack delivery) | scheduled-reports.md |
@@ -125,7 +126,7 @@ Workflow rows below are generated in-app adapters; CLI hosts load the named work
125
126
 
126
127
  ## Commands
127
128
 
128
- Claude Code supplies the `graphit` wrapper. On Codex, Cursor, terminals and CI, use `npx -y @graphit/cli@0.2.379 <command>`; pin a version for reproducibility. The table is generated from the CLI; check command help for exact flags.
129
+ Claude Code supplies the `graphit` wrapper. If it is missing, and on Codex, Cursor, terminals and CI, use the pinned `npx -y @graphit/cli@0.2.384 <command>`. The table is generated from the CLI; check command help for exact flags.
129
130
 
130
131
  <!-- COMMANDS:START -->
131
132
 
@@ -154,14 +155,14 @@ _Generated by `npm run gen:commands`; do not hand-edit between the markers._
154
155
  - `kb repo token mint` - Mint a display-once CI machine token (org admin); scope kb:verify or kb:apply - `--scope`
155
156
  - `kb repo token list` - List CI machine tokens: metadata only, never the secret
156
157
  - `kb repo token revoke <token-id>` - Revoke a CI machine token (org admin)
157
- - `kb batch` - Apply many metric and semantic-model creates and updates as one Knowledge Base change (up to 50 per batch; 20 in-app). Items: {op: create, noun, definition, unverified?} or {op: update, noun, name, patch}; each passes the same checks as kb create/kb update and reports its own result, and readers rebuild once for the whole batch instead of once per edit. Groups, rules and deletes use their own verbs - `--file --json --stop-on-error`
158
+ - `kb batch` - Apply many metric and semantic-model creates and updates as one Knowledge Base change (up to 50 per batch; 20 in-app). Items: {op: create, noun, definition} or {op: update, noun, name, patch}; each passes the same checks as kb create/kb update and reports its own result, and readers rebuild once for the whole batch instead of once per edit. Groups, rules and deletes use their own verbs - `--file --json --stop-on-error`
158
159
  - `kb template list` - List chart templates without their HTML
159
160
  - `kb template get <name>` - Fetch one chart template with its HTML. What expands in every adopting dashboard
160
161
  - `kb template create` - Create a chart template from an HTML fragment. A fragment may carry <script> and <style> and {{param}} placeholders in markup, never data-graphit-id/-sql/-ds/-label/-vocab/-state attributes: the host entity owns the query - `--name --file --json --description --params`
161
162
  - `kb template update <name>` - Update a chart template. A new fragment reaches every adopting dashboard on its next open - `--file --json --description --params`
162
163
  - `kb template delete <name>` - Delete a chart template (requires --yes). Adopting hosts render a missing marker - `--yes`
163
- - `kb create semantic-model` - Create a semantic model from a JSON definition (dbt shape: name, model, entities, dimensions, measures, defaults, group) - `--file --json --unverified`
164
- - `kb create metric` - Create a metric from a JSON definition (type: simple, ratio or derived, with type_params; advanced shapes remain unavailable). --family/--axis tag a concrete member of a metric family - `--file --json --family --axis --unverified`
164
+ - `kb create semantic-model` - Create a semantic model from a JSON definition (dbt shape: name, model, entities, dimensions, measures, defaults, group) - `--file --json`
165
+ - `kb create metric` - Create a metric from a JSON definition (type: simple, ratio or derived, with type_params; advanced shapes remain unavailable). --family/--axis tag a concrete member of a metric family - `--file --json --family --axis`
165
166
  - `kb create group` - Create a group (the domain analogue; admin only) - `--name --description --owner-email --access`
166
167
  - `kb create rule` - Create a retained Graphit governance rule from JSON. Targets use model:, entity:, dimension:, metric: or group: identities - `--file --json`
167
168
  - `kb update <noun> <name>` - Update an asset with a JSON patch. On semantic-model, a provided entities/dimensions/measures list replaces the stored list whole; explicit meta replaces author metadata whole - `--file --json`
@@ -175,8 +176,6 @@ _Generated by `npm run gen:commands`; do not hand-edit between the markers._
175
176
  - `kb family <family>` - Expand a metric family; with --axis constraints, resolve to the one concrete member (ambiguity answers with the still-open axes) - `--axis`
176
177
  - `kb explore <noun> <name>` - Traverse semantic reach. metric shows models, entities, dimensions and variants; semantic-model/group show bound metrics, families and rules
177
178
  - `kb usage [type] [name]` - Reverse lookup: dashboards using a semantic metric/dimension or enforcing a rule. Facets supplied by position or flags AND together - `--metric --dimension --rule`
178
- - `kb verify <noun> <name>` - Verify a Knowledge Base asset
179
- - `kb unverify <noun> <name>` - Unverify a Knowledge Base asset
180
179
  - `kb column-visibility <model> <column> <state>` - Set whether queries may read one physical column of a semantic model; hidden = masked as NULL everywhere (dashboards, exports, the AI). Unhiding a PII-detector hide takes the source's creator or an org admin, is recorded with who set it, and is for false positives only
181
180
 
182
181
  **query**
@@ -190,6 +189,7 @@ _Generated by `npm run gen:commands`; do not hand-edit between the markers._
190
189
  **ds** - Data source management
191
190
  - `ds refresh-history <id>` - Show recent refresh runs for a data source with the Snowflake query id per run (status, time, rows, duration). Runs from before query-id capture - or a failure before any query ran - show 'not captured'. Read-only; no ds refresh-history delete. - `--limit`
192
191
  - `ds usage [id]` - Show which canvas dashboards and graphs read a data source. With an id: each dashboard you can open and its graphs, plus a count of graphs on dashboards you cannot open. Without an id: dashboard_count and graph_count for every source you can read (0 = no graph uses it). A graph counts when its data-graphit-ds names the source, its governed metrics or dimensions come from the source's semantic model, or its SQL reads the source by name. Graphs composed only in JavaScript are not seen. Read-only; ds delete still re-checks metrics and rules.
192
+ - `ds sql-history <id>` - Show the Source SQL versions of a data source, newest first: when, by whom, from which channel (web, cli, agent, repo) and which columns each edit added (+), removed (-) or retyped (~). --show <n> prints version n's SQL. Keeps the last 50; history starts at the first edit after it shipped. Read-only - to reuse an old version, pass its SQL to ds edit-sql. - `--show`
193
193
  - `ds move <id>` - Not a command anywhere: a source lives in its bound semantic model's group; kb update semantic-model moves it
194
194
  - `ds delete <id>` - Delete one of YOUR OWN private data sources (requires --yes). Shared sources are deleted in the Sources Hub, where the cascade is visible. - `--yes`
195
195
  - `ds re-upload <id>` - Replace an uploaded CSV/Excel source's contents in place - keeps its id, graph bindings, semantic model and history; never re-create with `ds create --file`. A changed column set needs --force - `--file --force`
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@graphit/cli",
3
- "version": "0.2.379",
3
+ "version": "0.2.384",
4
4
  "source": "cli/package.json"
5
5
  }
@@ -16,7 +16,7 @@ The governed fragment path serves simple, ratio, and derived metrics. Cumulative
16
16
 
17
17
  ## Trust tiers
18
18
 
19
- - **governed:** verified semantic references compiled through the gateway.
19
+ - **governed:** valid accessible semantic references compiled through the gateway.
20
20
  - **verified:** known safe stored query without semantic references.
21
21
  - **ad hoc:** raw SQL at the frontier.
22
22
 
@@ -24,7 +24,7 @@ Prefer governed. Never present ad-hoc SQL as the team's definition.
24
24
 
25
25
  ## Rules
26
26
 
27
- Rules target model, entity, dimension, metric, or group identities. Verified constraints enforce; verified body-only rules guide; drafts do nothing. Modes and EXPLORE behavior remain server-owned.
27
+ Rules target model, entity, dimension, metric, or group identities. Saved constrained rules enforce according to their mode; body-only rules guide. Scope, source readiness and deprecation still apply. Modes and EXPLORE behavior remain server-owned.
28
28
 
29
29
  The gateway runs before caches, injects constraints, verifies resolved SQL, and returns a transparency receipt. Do not claim a rule applied merely because it exists.
30
30
 
@@ -6,7 +6,7 @@ Load when an approved gap must be authored or an existing semantic asset changed
6
6
 
7
7
  For a cached data source, first read its visible scanner-created semantic model and confirm `meta.graphit.data_source.ds_id` matches the source. Add approved entities, dimensions, or measures by updating that model. If no bound model is visible, follow the scan/verify flow in `data-sources.md`; creating a second model does not bind it.
8
8
 
9
- Before writing, present the missing concept, proposed root, exact definition, group/access scope, and verification state. Do not write until the user approves.
9
+ Before writing, present the missing concept, proposed root, exact definition and group/access scope. Do not write until the user approves.
10
10
 
11
11
  ## Authoring contract
12
12
 
@@ -16,7 +16,7 @@ Before writing, present the missing concept, proposed root, exact definition, gr
16
16
  - Entities, dimensions, and measures mutate only through semantic-model update.
17
17
  - A supplied nested list replaces the stored list whole. Read first and include every sibling that must remain.
18
18
  - Explicit `meta` replaces author metadata whole. Preserve family, axes, topics, and other author fields.
19
- - Use dedicated verify/unverify actions. Never patch metadata merely to change verification.
19
+ - Valid authorized saves are effective in their permitted scope. Do not send retired verification metadata or call manual verify/unverify actions.
20
20
 
21
21
  When create is refused because a model already binds that physical table, read the visible model named in the refusal and propose the needed update. Do not retry with another name or scope. If the response names no readable model, report the refusal without guessing or exposing a hidden target.
22
22
 
@@ -36,15 +36,15 @@ Constraints keep their five semantics: required predicate, forbidden column, req
36
36
  2. Preserve complete nested and metadata structures.
37
37
  3. Apply the smallest patch.
38
38
  4. Re-read immediately.
39
- 5. Verify/unverify separately when intended.
39
+ 5. Read back the saved definition and confirm its intended scope.
40
40
  6. Inspect receipts; a degraded write may have landed and must not be retried blindly.
41
41
 
42
42
  When one task creates or changes several metrics or semantic models, send them as one `graphit kb batch` instead of one `kb create` or `kb update` each. Every item gets the same checks and its own result, and dashboards rebuild once for the whole batch instead of once per edit, so they keep serving while you author. A single edit stays `kb update`. Steps 1-2 still apply to every item; a batch changes the transport, not the patch discipline.
43
43
 
44
- - Shape: `{"operations": [...]}` or a bare array of `{"op": "update", "noun", "name", "patch"}` and `{"op": "create", "noun", "definition", "unverified"?}` items. Nouns are `metric` and `semantic-model` only; groups, rules and deletes keep their own verbs. Up to 50 items per batch, 20 in-app.
44
+ - Shape: `{"operations": [...]}` or a bare array of `{"op": "update", "noun", "name", "patch"}` and `{"op": "create", "noun", "definition"}` items. Nouns are `metric` and `semantic-model` only; groups, rules and deletes keep their own verbs. Up to 50 items per batch, 20 in-app.
45
45
  - Order items so each validates against the ones before it: a semantic model before the metrics that use its measures, a metric before a derived metric over it. Use `--stop-on-error` when later items depend on earlier ones.
46
46
  - Items are not all-or-nothing: earlier items stay applied when a later one fails. Read `results` per item, fix what each `error` names, and re-send only the `failed` and `not_attempted` items as a new batch. Never replay the whole batch. An `unknown` item may have landed; read it back with `kb get` before sending it again.
47
- - In-app, approving the batch verifies each updated item, as approving a single update does.
47
+ - Each successful item is saved with the same validation and authorization as a single update.
48
48
 
49
49
  ## Delete
50
50
 
@@ -49,4 +49,4 @@ Use `{{ Metric('revenue') }}`, `{{ Dimension('order__channel') }}`, and `{{ Meas
49
49
 
50
50
  ## Gap decision
51
51
 
52
- Research first, then recommend. Show which existing definitions or dashboards already cover the request, what can be reused or extended, and what is still missing. Propose authoring only for that agreed gap, with formula, grain, binding, group, rule impact and verification. Ask when a choice is unresolved; do not treat a request to investigate as permission to build.
52
+ Research first, then recommend. Show which existing definitions or dashboards already cover the request, what can be reused or extended, and what is still missing. Propose authoring only for that agreed gap, with formula, grain, binding, group, rule impact and validation. Ask when a choice is unresolved; do not treat a request to investigate as permission to build.
@@ -10,7 +10,7 @@ Each member carries:
10
10
  - `meta.graphit.family`
11
11
  - axis key/value pairs in `meta.graphit.axes`
12
12
 
13
- Create each concrete member with the family and repeatable axis options. Tree/search/family views collapse members; `list metric` remains flat.
13
+ Create each concrete member with the family and repeatable axis options. For several members, send one `graphit kb batch` (kb-actions.md) whose definitions carry the same metadata: `"meta": {"graphit": {"family": "arppu", "axes": {"horizon": "d7"}}}`. Tree/search/family views collapse members; `list metric` remains flat.
14
14
 
15
15
  Use family expansion to inspect members. Supply known axes to resolve. If several candidates remain, show open axes and ask—never guess a governed metric.
16
16
 
@@ -56,7 +56,7 @@ Shape it for the question - grain, only the columns dashboards use, low cardinal
56
56
 
57
57
  ## 4. Create the KB assets
58
58
 
59
- The scan supplies the bound model. Explore answers without authoring definitions; Private first Build uses it and keeps a private metric only on request. Share applies the readiness gate: show missing prerequisites and proposed definitions, obtain required approval, then create and verify via kb-structure.md and kb-actions.md. Onboarding does not override the selected workflow.
59
+ The scan supplies the bound model. Explore answers without authoring definitions; Private first Build uses it and keeps a private metric only on request. Share applies the readiness gate: show missing prerequisites and proposed definitions, obtain required approval, then save and read back via kb-structure.md and kb-actions.md. Onboarding does not override the selected workflow.
60
60
 
61
61
  ## 5. Offer a dashboard
62
62
 
@@ -0,0 +1,38 @@
1
+ # Page Load: Request Graph and Readiness
2
+
3
+ Load when a dashboard sends more than six requests on open (charts, option lists, bounds) or has a query feeding another (a rank feeding a series, bounds feeding a date window); when its open or Apply time is slow or measured; or when declaring readiness. `runtime.md` still owns the resolve API, the entity contract and the rate budget.
4
+
5
+ Every `graphit.resolve()` and chrome call (`cascade`, `dataBounds`, `rank`) pays a fixed server cost of up to about a second, and the SDK sends at most six at once, in call order. Load time is roughly that cost times the rounds a page waits through, so shape the request graph before tuning SQL.
6
+
7
+ ## The request graph
8
+
9
+ - **Parallel unless the rows are needed.** Start independent queries together in one `Promise.all`. Await one query before another only when the later query's SQL or params use the earlier result. A chain of awaits whose later steps ignore earlier results turns one round into many.
10
+ - **Fold a dependency when you can.** A rank that only picks the top N for a series is usually one statement: rank in a CTE and join the series to it. Keep two steps when the first result is also shown.
11
+ - **Visible first.** Start above-the-fold charts and KPIs before option lists, bounds for secondary controls and anything below the fold; later calls wait behind earlier ones.
12
+ - **Hidden tabs wait** until first shown (`runtime.md`, "Declare statically, execute lazily").
13
+ - **One first load.** `graphit.state.subscribe` calls back immediately: guard the callback with a boot flag and call `refresh()` once when init ends (`state-contract.md`, "Restore order"). An unguarded callback plus a boot refresh runs every query twice.
14
+ - **Redraw, don't re-resolve.** Resize, highlight, legend, axis and zero-axis toggles change drawing only: keep the last result and redraw from it. Resolve again only when a filter, param or variant changes the query.
15
+
16
+ ## Declaring readiness
17
+
18
+ Declare what "loaded" means so the page reports its open and Apply time. Readiness never delays drawing, and a missed acknowledgement only records a timeout after two minutes. Misuse does throw, so build `targets` from the same ids you pass tokens for.
19
+
20
+ 1. At the start of the open and of each Apply, call `graphit.readiness.begin(kind, targets)`: `kind` is `'initial'` or `'apply'`, `targets` the `data-graphit-id`s this load paints (1-128, unique). A hidden tab's entities are not targets until it opens. Each Apply supersedes the open and the previous Apply. `begin` throws on another kind or a bad list, and `ready.target(id)` on an id missing from `targets`.
21
+ 2. Pass `readiness: ready.target(id)` to each `graphit.resolve`, `dataBounds` or `rank` call for that target and to its `graphit.graph`, `table` or `kpi` render, which acknowledges the paint itself. After drawing custom DOM or options, call `ready.target(id).rendered()`. A `bind` keeps the token it was registered with, so it reports only that first load; give Apply targets through `resolve`.
22
+ 3. Call `ready.stateReady()` once the filters and params this load uses are applied, and `ready.fail()` if the load fails.
23
+
24
+ ```js
25
+ function load(kind) {
26
+ const ready = graphit.readiness.begin(kind, ['spend-trend', 'spend-kpi']);
27
+ ready.stateReady(); // this page applies its state before load()
28
+ return Promise.all([
29
+ graphit.resolve({ target: '#spend-trend', readiness: ready.target('spend-trend') })
30
+ .then(r => graphit.graph('#spend-trend-chart', { type: 'line', data: r.data, x: 'day', y: 'spend',
31
+ readiness: ready.target('spend-trend') })),
32
+ graphit.resolve({ target: '#spend-kpi', readiness: ready.target('spend-kpi') })
33
+ .then(r => graphit.kpi('#spend-kpi-card', { value: r.data[0]?.spend ?? 0, readiness: ready.target('spend-kpi') })),
34
+ ]).catch(() => ready.fail());
35
+ }
36
+ ```
37
+
38
+ Redraw-only toggles begin nothing. Never run a query just to measure readiness.
@@ -140,11 +140,11 @@ A resolve query following these shapes serves from a semantic cache in roughly 1
140
140
 
141
141
  `graphit.resolve()` is rate-limited per user per dashboard: 360 requests a minute, 180 of them cold executions (a cache hit is not one). Design for that budget:
142
142
 
143
- - **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.
144
- - **Count queries per interaction.** 6 charts is 6 cold executions per filter change, about 30 changes a minute of budget; 12 charts is about 15. With 10 or more charts and 3 or more filters, debounce filter changes (300ms).
145
- - **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`.
146
- - **Avoid redundant refreshes.** If a filter affects only some charts, split into targeted refresh functions (`refreshKPIs()`, `refreshCharts()`).
147
- - **No polling.** NEVER use `setInterval(refresh, ...)`. Data sources update on their own schedule; a polling dashboard burns the entire budget.
143
+ - **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 handlers or timeouts - one user action becomes several bursts.
144
+ - **Count queries per interaction.** 6 charts cost 6 cold executions per filter change, about 30 changes a minute; 12 charts, about 15. With 10 or more charts and 3 or more filters, debounce filter changes (300ms).
145
+ - **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, not a separate aggregate query. Anchor the extra graphs it feeds with `targetEntityIds` per the attribution rule above. Canonical KPI-row example: `kpi.md`.
146
+ - **Avoid redundant refreshes.** If a filter affects only some charts, split into targeted refresh functions (`refreshKPIs()`, `refreshCharts()`). Resize, highlight and axis toggles only redraw; no resolve (`page-load.md`).
147
+ - **No polling.** NEVER use `setInterval(refresh, ...)`. Data sources refresh on their own schedule; polling burns the whole budget.
148
148
 
149
149
  ## Helper index
150
150
 
@@ -22,7 +22,7 @@ Graphit extensions and assets:
22
22
  - topics are curated Graphit metadata, deliberately not dbt `tags`
23
23
  - rules are separate Graphit objects targeting semantic identities by name;
24
24
  never embed them in dbt metadata
25
- - verification attribution, provenance, and data-source bindings are
25
+ - provenance and data-source bindings are
26
26
  server-owned; use dedicated actions instead of hand-authoring them
27
27
 
28
28
  Do not expose or depend on storage collection names, revision fields, feature
@@ -76,7 +76,7 @@ Measure identity is group/model/measure, never a bare name. A metric's measure r
76
76
 
77
77
  Explore authors no definitions. Private first Build creates a metric only on "keep": use the scanner model's exact private group (`kb-scope.md`), with no group-agreement or shared-reuse approval round. The comparisons below still apply. In Share, follow `kb-discovery.md`: agree the group, inspect assets/cross-group matches and present reuse-or-build. Before missing inputs, discover visible candidates in the target group/model. Summaries only nominate candidates; follow continuation metadata before declaring a gap. Ranked search or an incomplete page is not proof of absence.
78
78
 
79
- Read each plausible metric's full definition and its reached semantic models. Compare the resolved model/source binding, grain and time dimension, measure expression and aggregation parameters, metric-level and per-input filters, units/scale, verification state, ownership, and applicable rules. Similar names or identical SQL alone are insufficient. Use the existing path resolution above; never inspect hidden definitions or copy a private definition into a shared scope to make it reusable.
79
+ Read each plausible metric's full definition and its reached semantic models. Compare the resolved model/source binding, grain and time dimension, measure expression and aggregation parameters, metric-level and per-input filters, units/scale, ownership, and applicable rules. Similar names or identical SQL alone are insufficient. Use the existing path resolution above; never inspect hidden definitions or copy a private definition into a shared scope to make it reusable.
80
80
 
81
81
  | Finding | Action |
82
82
  |---|---|
@@ -85,13 +85,13 @@ Read each plausible metric's full definition and its reached semantic models. Co
85
85
  | Different grain, filters, scale, binding or applicable policy | Keep the definitions separate; ask if the intended business meaning is unclear |
86
86
  | Repository-owned definition needs a change | Follow the repository authoring workflow; do not create a direct-write replacement to bypass ownership |
87
87
 
88
- A ratio still references numerator/denominator metric objects. For example, a verified total-matches metric can serve several ratios at the same grain; a country-filtered matches metric is not an interchangeable denominator for all countries. Being referenced or ending in `_num`/`_den` does not make an existing metric disposable.
88
+ A ratio still references numerator/denominator metric objects. For example, a saved total-matches metric can serve several ratios at the same grain; a country-filtered matches metric is not an interchangeable denominator for all countries. Being referenced or ending in `_num`/`_den` does not make an existing metric disposable.
89
89
 
90
- After discovery, author only the approved missing prerequisites: group, data sources, semantic models with nested components, simple metrics, ratio/derived metrics, then rules after their targets exist. Execute one item at a time; do not start the next before the current receipt is terminal.
90
+ After discovery, author only the approved missing prerequisites: group, data sources, semantic models with nested components, simple metrics, ratio/derived metrics, then rules after their targets exist. Execute in that order: metrics and semantic models as one ordered `graphit kb batch` (kb-actions.md); start any other item only after the current receipt is terminal.
91
91
 
92
92
  ## Verification
93
93
 
94
- Create defaults to verified on human-driven CLI paths; `--unverified` creates a draft. Promote or demote with dedicated verify/unverify actions. Never replace `meta` only to toggle verification.
94
+ Valid authorized saves are effective within their access scope. Manual KB verification and draft status are retired; omit `unverified`, `verified`, `verified_by`, and `verified_at`. Source readiness, schema acceptance and SQL validation still apply.
95
95
 
96
96
  ## Final check
97
97