@graphit/cli 0.2.380 → 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 (41) 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/sync-plugin-marketplace.sh +18 -0
  26. package/scripts/verb-policy-source.json +49 -30
  27. package/scripts/verify-kb-cutover.mjs +134 -0
  28. package/skills/graphit/SKILL.md +7 -7
  29. package/skills/graphit/VERSION.json +1 -1
  30. package/skills/graphit/references/governance.md +2 -2
  31. package/skills/graphit/references/kb-actions.md +5 -5
  32. package/skills/graphit/references/kb-discovery.md +1 -1
  33. package/skills/graphit/references/metric-families.md +1 -1
  34. package/skills/graphit/references/onboarding.md +1 -1
  35. package/skills/graphit/references/page-load.md +38 -0
  36. package/skills/graphit/references/runtime.md +5 -5
  37. package/skills/graphit/references/semantic-authoring.md +5 -5
  38. package/skills/graphit/references/share.md +4 -4
  39. package/skills/graphit-build/SKILL.md +1 -1
  40. package/skills/graphit-explore/SKILL.md +1 -1
  41. package/skills/graphit-share/SKILL.md +5 -5
@@ -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.380"
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.380 <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.380",
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
 
@@ -20,9 +20,9 @@ Use the **draft door** for shared dashboards and the **share plan** for private
20
20
 
21
21
  ## Shared-scope checks
22
22
 
23
- Read kb-scope.md for effective permissions and exact placement, kb-discovery.md for staged reuse discovery, and semantic-authoring.md plus kb-actions.md for supported definitions, equivalence and verification. Resolve the audience, lowercase group and uppercase policy key from current `status` and returned `domain_keys`; carry forward choices already made. Read access is the ceiling for writes, and a status result is advisory, never a grant.
23
+ Read kb-scope.md for effective permissions and exact placement, kb-discovery.md for staged reuse discovery, and semantic-authoring.md plus kb-actions.md for supported definitions, equivalence and validation. Resolve the audience, lowercase group and uppercase policy key from current `status` and returned `domain_keys`; carry forward choices already made. Read access is the ceiling for writes, and a status result is advisory, never a grant.
24
24
 
25
- **KB-readiness gate:** before work goes live for others, confirm the required models, nested components, metrics, groups and rules exist and have the needed verification. If a business measure is missing, present its gap and proposed governed definition for approval, then author and verify the approved prerequisites. An ad-hoc business measure can be unavailable to governed-only viewers; do not silently publish it as a reusable governed answer. Compare actual binding, grain, time dimension, aggregation, filters, units and policy, not just names or SQL. A same-named conflicting asset is not equivalent: explain the difference and resolve the consequential choice. A truly equivalent accessible asset should be reused.
25
+ **KB-readiness gate:** before work goes live for others, confirm the required models, nested components, metrics, groups and rules exist, are valid and are readable in the intended scope. If a business measure is missing, present its gap and proposed governed definition for approval, then save and read back the approved prerequisites. An ad-hoc business measure can be unavailable to governed-only viewers; do not silently publish it as a reusable governed answer. Compare actual binding, grain, time dimension, aggregation, filters, units and policy, not just names or SQL. A same-named conflicting asset is not equivalent: explain the difference and resolve the consequential choice. A truly equivalent accessible asset should be reused.
26
26
 
27
27
  Choose dashboard audience and folder through dashboard-create.md when sharing. Owners share their own; org admins/owners also any they can see. Org needs admin/owner; Team needs membership. When needed, explain Private/ORG/named scopes via kb-scope.md; keep dashboard audience separate.
28
28
 
@@ -37,9 +37,9 @@ Report 409 (another editor), 423 (locked) and 403 (view-only) with the returned
37
37
  ## Share plan: private to shared
38
38
 
39
39
  1. With the requested audience and placement established and the content checked, attempt `dashboard share` on the same ID. A success needs audience and placement readback; a private-dependency refusal supplies `blockers` and `remediation_options`. Read sharing-recovery.md. Respect `blockers_truncated` and uncertainty; the visible list is not proof of a complete closure when eligibility could not be verified. For shapes b/c without a dashboard, inspect the selected assets and their visible dependencies directly; do not invent a share-preview endpoint.
40
- 2. For each returned private dependency, choose **reuse**, **move**, or **create**. Reuse an accessible shared equivalent only after inspecting its full definition. Move an approved model or metric with `kb update semantic-model` or `kb update metric` and the target `group`; the bound source's home follows its model, there is no independent source-move command. Create only a genuinely missing, supported definition through `kb create`, never a copy to evade ownership or a refusal. Include naming collisions and the impact of changing visibility.
40
+ 2. For each returned private dependency, choose **reuse**, **move**, or **create**. Reuse an accessible shared equivalent only after inspecting its full definition. Move an approved model or metric with `kb update semantic-model` or `kb update metric` and the target `group`, several in one ordered `kb batch`; the bound source's home follows its model, no command moves a source alone. Create only a genuinely missing, supported definition through `kb create`, never a copy to evade ownership or a refusal. Include naming collisions and visibility impact.
41
41
  3. Present the whole plan as **one structured ask**: exact assets, reuse comparisons, moves/new definitions, affected audience, group, rules and destination. Carry forward existing authorization; ask for the additional effects or consequential choices not yet approved. An authorization to share a dashboard alone does not silently authorize broadening every source's audience.
42
- 4. Apply the approved dependency order one item at a time. Read each terminal receipt and re-read the resulting definition/binding before the next step. When reusing a shared asset, rewrite each `data-graphit-*` attribute naming the replaced asset through `dashboard update-html`, preserving unrelated content. Run the readiness checks on the resulting references and pre-flight the canvas. `dashboard check` is not proof of sharing eligibility.
42
+ 4. Apply the approved dependency order. Read each item's terminal receipt and re-read the resulting definition/binding before the next step. When reusing a shared asset, rewrite each `data-graphit-*` attribute naming the replaced asset through `dashboard update-html`, preserving unrelated content. Run the readiness checks on the resulting references and pre-flight the canvas. `dashboard check` is not proof of sharing eligibility.
43
43
  5. Retry the original `dashboard share` with the agreed space/team and `--folder-path`. Verify the same ID's audience through `dashboard list` and placement through the destination folder listing. For definitions/source-only work, read back the exact group, binding and requested configuration instead of claiming a dashboard was shared.
44
44
  6. On intermediate failure, report what applied, what remains and the returned next step. Preserve successful work. Reconcile uncertain writes by reading state before retrying; do not repeat a non-retryable operation unchanged, silently fall back to another folder, or create a replacement dashboard/source.
45
45
 
@@ -2,7 +2,7 @@
2
2
  name: graphit-build
3
3
  description: >-
4
4
  Author and verify Graphit dashboard content, private or shared, and build private reports, sources and saved metrics. Pair with graphit-share for shared dependencies, draft sessions and publication. Use graphit-explore for answers without artifacts.
5
- skill_version: "0.2.380"
5
+ skill_version: "0.2.384"
6
6
  ---
7
7
 
8
8
  # Build: author and verify content
@@ -2,7 +2,7 @@
2
2
  name: graphit-explore
3
3
  description: >-
4
4
  Answer, explain or diagnose business data using Graphit. Use after Graphit routing or for a direct Graphit question, including reads of shared dashboards. Does not authorize creating reusable definitions or sharing; use graphit-build to keep a private artifact and graphit-share for shared writes.
5
- skill_version: "0.2.380"
5
+ skill_version: "0.2.384"
6
6
  ---
7
7
 
8
8
  # Explore: answer the question