chartwright 0.1.0__tar.gz → 0.2.0__tar.gz

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 (80) hide show
  1. {chartwright-0.1.0/chartwright.egg-info → chartwright-0.2.0}/PKG-INFO +87 -42
  2. {chartwright-0.1.0 → chartwright-0.2.0}/README.md +85 -40
  3. {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/__init__.py +1 -1
  4. {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/absorb.py +1 -0
  5. {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/apply.py +80 -15
  6. chartwright-0.2.0/chartwright/cli.py +444 -0
  7. {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/client.py +60 -3
  8. {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/compiler.py +219 -24
  9. {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/dashdiff.py +33 -5
  10. {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/decompile.py +316 -24
  11. chartwright-0.2.0/chartwright/design/__init__.py +129 -0
  12. chartwright-0.2.0/chartwright/design/brief.py +101 -0
  13. chartwright-0.2.0/chartwright/design/calibrate.py +139 -0
  14. chartwright-0.2.0/chartwright/design/fix.py +74 -0
  15. chartwright-0.2.0/chartwright/design/guidelines/chart-choice.md +31 -0
  16. chartwright-0.2.0/chartwright/design/guidelines/composition.md +28 -0
  17. chartwright-0.2.0/chartwright/design/model.py +326 -0
  18. chartwright-0.2.0/chartwright/design/presets.py +181 -0
  19. chartwright-0.2.0/chartwright/design/probe.py +61 -0
  20. chartwright-0.2.0/chartwright/design/redesign.py +53 -0
  21. chartwright-0.2.0/chartwright/design/rules.py +1097 -0
  22. chartwright-0.2.0/chartwright/mcp_server.py +288 -0
  23. {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/profiles.py +96 -0
  24. {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/resolver.py +67 -9
  25. {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/sketch.py +10 -0
  26. {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/smoke.py +60 -3
  27. {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/spec.py +302 -44
  28. {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/testing.py +24 -0
  29. {chartwright-0.1.0 → chartwright-0.2.0/chartwright.egg-info}/PKG-INFO +87 -42
  30. {chartwright-0.1.0 → chartwright-0.2.0}/chartwright.egg-info/SOURCES.txt +33 -0
  31. {chartwright-0.1.0 → chartwright-0.2.0}/chartwright.egg-info/requires.txt +1 -1
  32. {chartwright-0.1.0 → chartwright-0.2.0}/pyproject.toml +6 -3
  33. chartwright-0.2.0/tests/test_apply_rollback.py +119 -0
  34. chartwright-0.2.0/tests/test_calibrate.py +108 -0
  35. {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_client.py +13 -0
  36. chartwright-0.2.0/tests/test_column_suggestions.py +64 -0
  37. chartwright-0.2.0/tests/test_cross_filters.py +84 -0
  38. {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_decompile.py +1 -1
  39. chartwright-0.2.0/tests/test_design.py +471 -0
  40. chartwright-0.2.0/tests/test_design_arch.py +221 -0
  41. chartwright-0.2.0/tests/test_design_data.py +140 -0
  42. chartwright-0.2.0/tests/test_design_gate.py +64 -0
  43. chartwright-0.2.0/tests/test_design_rules_v2.py +213 -0
  44. chartwright-0.2.0/tests/test_docs.py +120 -0
  45. chartwright-0.2.0/tests/test_layout_footer.py +137 -0
  46. chartwright-0.2.0/tests/test_markdown_heights.py +59 -0
  47. chartwright-0.2.0/tests/test_mcp_server.py +162 -0
  48. chartwright-0.2.0/tests/test_mixed_chart.py +233 -0
  49. chartwright-0.2.0/tests/test_nested_tabs.py +101 -0
  50. {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_pivot_formatting.py +50 -8
  51. chartwright-0.2.0/tests/test_plan_errors.py +56 -0
  52. chartwright-0.2.0/tests/test_preset_auth.py +179 -0
  53. {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_range_filter.py +67 -1
  54. chartwright-0.2.0/tests/test_redesign.py +67 -0
  55. chartwright-0.2.0/tests/test_review_tier_1_2.py +214 -0
  56. chartwright-0.2.0/tests/test_select_filter.py +133 -0
  57. chartwright-0.2.0/tests/test_smoke_fit.py +78 -0
  58. {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_spec_v2.py +1 -1
  59. chartwright-0.2.0/tests/test_table_formatting.py +169 -0
  60. chartwright-0.2.0/tests/test_table_sort.py +147 -0
  61. chartwright-0.1.0/chartwright/cli.py +0 -222
  62. chartwright-0.1.0/chartwright/mcp_server.py +0 -112
  63. chartwright-0.1.0/tests/test_mcp_server.py +0 -54
  64. {chartwright-0.1.0 → chartwright-0.2.0}/LICENSE +0 -0
  65. {chartwright-0.1.0 → chartwright-0.2.0}/NOTICE +0 -0
  66. {chartwright-0.1.0 → chartwright-0.2.0}/chartwright/ids.py +0 -0
  67. {chartwright-0.1.0 → chartwright-0.2.0}/chartwright.egg-info/dependency_links.txt +0 -0
  68. {chartwright-0.1.0 → chartwright-0.2.0}/chartwright.egg-info/entry_points.txt +0 -0
  69. {chartwright-0.1.0 → chartwright-0.2.0}/chartwright.egg-info/top_level.txt +0 -0
  70. {chartwright-0.1.0 → chartwright-0.2.0}/setup.cfg +0 -0
  71. {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_absorb.py +0 -0
  72. {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_backup_layout.py +0 -0
  73. {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_compiler.py +0 -0
  74. {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_dashdiff.py +0 -0
  75. {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_params_contract.py +0 -0
  76. {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_profiles.py +0 -0
  77. {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_property_roundtrip.py +0 -0
  78. {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_sketch.py +0 -0
  79. {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_spec.py +0 -0
  80. {chartwright-0.1.0 → chartwright-0.2.0}/tests/test_time_filter.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chartwright
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Build, verify, and maintain Apache Superset dashboards from small spec files.
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Homepage, https://github.com/debabsah/chartwright
@@ -24,14 +24,16 @@ Requires-Dist: pyyaml>=6.0
24
24
  Provides-Extra: dev
25
25
  Requires-Dist: pytest>=8; extra == "dev"
26
26
  Provides-Extra: mcp
27
- Requires-Dist: mcp>=1.0; extra == "mcp"
27
+ Requires-Dist: mcp<3,>=1.0; extra == "mcp"
28
28
  Dynamic: license-file
29
29
 
30
30
  # Chartwright
31
31
 
32
- A dashboard compiler for Apache Superset. Describe the dashboard once, in a
33
- small text file called a spec, and the `chartwright` command line creates it,
34
- verifies it, and keeps it that way.
32
+ A dashboard compiler for Apache Superset. The `chartwright` command line builds
33
+ a dashboard from a small deterministic file, updates it from that file later,
34
+ and decompiles an existing dashboard back into one. Every dataset, column, and
35
+ metric is checked against your own Superset first. An AI can write the file for
36
+ you, or revise it, from a request in plain words.
35
37
 
36
38
  ![An operations dashboard over NYC yellow-taxi data: KPI cards with unit subtitles, daily trend charts, an hour-by-weekday demand heatmap, a payment donut, borough and zone rankings, and a trip-distance histogram, behind a three-filter bar](https://raw.githubusercontent.com/debabsah/chartwright/main/docs/images/nyc-taxi-operations.png)
37
39
 
@@ -41,14 +43,20 @@ charts, three filters, one `chartwright apply`. An AI wrote
41
43
  [the request and rebuild steps](https://github.com/debabsah/chartwright/blob/main/examples/README.md).*
42
44
 
43
45
  A dashboard that lives in a file gets the workflow code already has: review it
44
- in a pull request, rebuild it identically, diff it against what is live, and
45
- bring it back after a bad change. Write the spec yourself or ask an AI for one;
46
- either way, every dataset, column, and metric the spec names is confirmed to
47
- exist before anything is built, and every chart is checked to load with data
48
- after.
46
+ in a pull request, rebuild it identically, and bring it back after a bad
47
+ change. `chartwright plan` diffs the file against the live dashboard and exits
48
+ non-zero when they differ, so drift fails a CI check. `chartwright decompile`
49
+ covers the other direction, and lists anything it could not carry over.
50
+
51
+ Write the file yourself, or describe the dashboard you want and have an AI
52
+ write it. The same checks run either way. After the build, each chart's query
53
+ runs once (an error fails the build, an empty chart is named), and
54
+ `chartwright advise` reviews the layout for readability.
49
55
 
50
56
  ## What a spec looks like
51
57
 
58
+ The file is called a spec:
59
+
52
60
  ```json
53
61
  {
54
62
  "spec_version": "1",
@@ -75,8 +83,24 @@ after.
75
83
  `chartwright apply` on this file signs in to your Superset, confirms the
76
84
  `cleaned_sales_data` table and the `sales` and `order_date` columns exist,
77
85
  builds a KPI card above a line chart, and finishes by running each chart's
78
- query once to prove it shows data. A misspelled column or a missing table is a
79
- clear error naming the problem, before anything is created.
86
+ query once. A misspelled column or a missing table is a clear error naming the
87
+ problem, with the closest real names suggested, before anything is created.
88
+
89
+ ## Creating dashboards with AI
90
+
91
+ The spec is typed, and every reference in it is checked before anything is
92
+ created. Whatever a model proposes has to survive the same verification your
93
+ own specs do.
94
+
95
+ - **Claude Code skill**: from a clone of this repo, run
96
+ `python install-skill.py`; the model writes the spec from your request, and
97
+ the tool verifies and builds it.
98
+ - **MCP server**: `chartwright-mcp` (installed with `pip install "chartwright[mcp]"`)
99
+ exposes ten tools covering the whole lifecycle, usable from any MCP client.
100
+ - **Open contract**: `chartwright schema` prints the spec's JSON Schema, so any LLM or
101
+ tool can generate valid specs.
102
+ - **Guardrails**: the AI proposes; the tool verifies, using your own Superset
103
+ login. Verification reads names (datasets, columns, metrics), not rows.
80
104
 
81
105
  ## Quick start
82
106
 
@@ -113,35 +137,47 @@ as-is.
113
137
  - **Deterministic dashboards**: the same spec always produces the identical
114
138
  dashboard. Diff it in git, review it in a PR.
115
139
  - **Verified at every step**: every reference is checked before anything is
116
- written, and every chart's query runs once to prove it shows data; failures
117
- say what went wrong and where.
140
+ written, and every chart's query runs once (an error fails the build, an
141
+ empty chart is named); failures say what went wrong and where.
118
142
  - **Dashboards as code, in both directions**: `chartwright decompile` turns any live
119
143
  dashboard into a spec; `chartwright plan` shows what differs between the spec and
120
144
  the live dashboard, ready as a CI gate; `chartwright compile` builds the import
121
145
  bundle offline, no server needed.
122
- - **Safety built in**: every apply backs up the previous state first; a failed
123
- apply restores it automatically; the tool only ever overwrites dashboards it
124
- created, and a hand-built dashboard is adopted by decompiling it into a spec
125
- first.
126
- - **The full design surface**: 14 chart types, metrics as you write them,
127
- per-chart filters, a native filter bar, tabs, markdown notes, and layouts
128
- you can draw as ASCII sketches.
146
+ - **Recoverable by default**: every apply backs up the previous state first,
147
+ and an apply that fails while importing or updating charts restores it
148
+ automatically. Dashboards Chartwright did not create are never overwritten;
149
+ to bring a hand-built one under a spec, decompile it and build it at a new
150
+ slug.
151
+ - **What a spec can express**: 15 chart types, metrics as you write them,
152
+ per-chart filters, a native filter bar, tabs, markdown notes, a footer shown
153
+ under every tab, and layouts you can draw as ASCII sketches.
129
154
  - **Environment promotion**: specs name their data (connection, schema,
130
- table), so the same file applies to dev, staging, and production unchanged.
155
+ table), so the same file applies to dev, staging, and production when they
156
+ share connection names.
131
157
 
132
158
  Every capability, with the CLI verb reference: [docs/FEATURES.md](https://github.com/debabsah/chartwright/blob/main/docs/FEATURES.md).
133
159
 
134
- ## Creating dashboards with AI
160
+ ## The design brain
135
161
 
136
- - **Claude Code skill**: from a clone of this repo, `python install-skill.py`,
137
- then ask for a dashboard in plain words; the AI writes the spec, and the
138
- tool verifies and builds it.
139
- - **MCP server**: `chartwright-mcp` (installed with `pip install "chartwright[mcp]"`)
140
- exposes six tools covering the whole lifecycle, usable from any MCP client.
141
- - **Open contract**: `chartwright schema` prints the spec's JSON Schema, so any LLM or
142
- tool can generate valid specs.
143
- - **Guardrails**: the AI proposes; the tool verifies, using your own Superset
144
- login. Verification reads names (datasets, columns, metrics), not rows.
162
+ A spec can import perfectly and still produce a dashboard nobody can read: a
163
+ pie squeezed into two columns, a KPI buried under three tables, a bar chart
164
+ with forty category labels Superset silently drops. The design brain covers
165
+ size and scroll budgets, chart-choice limits, and layout composition. It
166
+ arrives as a brief the AI reads before authoring, and a critic that reviews
167
+ the result:
168
+
169
+ ```bash
170
+ chartwright brief --audience executive # the guidance, before writing a spec
171
+ chartwright advise spec.json --fix # the critique, with safe auto-repairs
172
+ chartwright redesign old-dash --profile prod # audit + repair a live dashboard
173
+ ```
174
+
175
+ You stay in charge of it: suppress any rule for one chart in the spec, tune
176
+ the thresholds per audience or per deployment in `design.yaml`, and feed the
177
+ heights you drag in the UI back into the recommendations with `chartwright
178
+ calibrate`. Pass `--design off` and the output is byte-identical to a build
179
+ that never had it. The full rulebook and architecture:
180
+ [docs/DESIGN-BRAIN.md](https://github.com/debabsah/chartwright/blob/main/docs/DESIGN-BRAIN.md).
145
181
 
146
182
  ## Drawing layouts as text
147
183
 
@@ -162,15 +198,19 @@ space under `K` deliberately empty. Every rule, drawn and explained:
162
198
 
163
199
  ## Testing and evidence
164
200
 
165
- Every push runs 125 tests on Linux and Windows, plus the full pipeline (apply,
166
- lifecycle soak, stale-tab adversary, fault injection) against real Superset
167
- 4.1.4, 5.0.0, and 6.1.0 containers. Chart options are checked against
168
- Superset's own source for every supported version, so a Superset change is
169
- caught in our tests before it reaches your dashboards.
201
+ Every pull request and every push to main runs the full offline suite on Linux and
202
+ Windows, plus the full pipeline (apply, lifecycle soak, stale-tab adversary,
203
+ fault injection) against real Superset 4.1.4, 5.0.0, and 6.1.0 containers.
204
+ Chart options are checked against Superset's own source for every supported
205
+ version, so a Superset change surfaces here before it reaches your dashboards.
206
+
207
+ Full evidence: [docs/VERIFICATION.md](https://github.com/debabsah/chartwright/blob/main/docs/VERIFICATION.md).
170
208
 
171
- Full evidence: [docs/VERIFICATION.md](https://github.com/debabsah/chartwright/blob/main/docs/VERIFICATION.md). Source citations
172
- for every Superset behavior the tool relies on:
173
- [docs/CONTRACTS.md](https://github.com/debabsah/chartwright/blob/main/docs/CONTRACTS.md).
209
+ [docs/CONTRACTS.md](https://github.com/debabsah/chartwright/blob/main/docs/CONTRACTS.md) records how Superset itself behaves: what its
210
+ importer accepts, how dashboard settings are stored, what its chart plugins
211
+ expect. Every behavior is cited to its source line at 4.1.4, 5.0.0, and 6.1.0.
212
+ Release tags are immutable, so any line of it can be checked against a fresh
213
+ checkout of that tag.
174
214
 
175
215
  ## Documentation
176
216
 
@@ -178,10 +218,15 @@ for every Superset behavior the tool relies on:
178
218
  reference
179
219
  - [docs/LAYOUT-GUIDE.md](https://github.com/debabsah/chartwright/blob/main/docs/LAYOUT-GUIDE.md): drawing layouts as text,
180
220
  every rule illustrated
221
+ - [docs/DESIGN-BRAIN.md](https://github.com/debabsah/chartwright/blob/main/docs/DESIGN-BRAIN.md): the design rulebook, every
222
+ rule and threshold, and how to tune or switch them off
181
223
  - [docs/VERIFICATION.md](https://github.com/debabsah/chartwright/blob/main/docs/VERIFICATION.md): what is tested, what it
182
224
  caught, and how to reproduce it
183
- - [docs/CONTRACTS.md](https://github.com/debabsah/chartwright/blob/main/docs/CONTRACTS.md): the Superset behaviors the tool
184
- depends on, cited to source at each supported version
225
+ - [docs/CONTRACTS.md](https://github.com/debabsah/chartwright/blob/main/docs/CONTRACTS.md): how Superset behaves on import,
226
+ on dashboard storage, and in chart options, cited to its source line at
227
+ each supported release
228
+ - [docs/COMPARISON.md](https://github.com/debabsah/chartwright/blob/main/docs/COMPARISON.md): what Chartwright, preset-cli,
229
+ sup, and the Terraform provider each automate, so you can pick by the job
185
230
 
186
231
  ---
187
232
 
@@ -1,8 +1,10 @@
1
1
  # Chartwright
2
2
 
3
- A dashboard compiler for Apache Superset. Describe the dashboard once, in a
4
- small text file called a spec, and the `chartwright` command line creates it,
5
- verifies it, and keeps it that way.
3
+ A dashboard compiler for Apache Superset. The `chartwright` command line builds
4
+ a dashboard from a small deterministic file, updates it from that file later,
5
+ and decompiles an existing dashboard back into one. Every dataset, column, and
6
+ metric is checked against your own Superset first. An AI can write the file for
7
+ you, or revise it, from a request in plain words.
6
8
 
7
9
  ![An operations dashboard over NYC yellow-taxi data: KPI cards with unit subtitles, daily trend charts, an hour-by-weekday demand heatmap, a payment donut, borough and zone rankings, and a trip-distance histogram, behind a three-filter bar](https://raw.githubusercontent.com/debabsah/chartwright/main/docs/images/nyc-taxi-operations.png)
8
10
 
@@ -12,14 +14,20 @@ charts, three filters, one `chartwright apply`. An AI wrote
12
14
  [the request and rebuild steps](https://github.com/debabsah/chartwright/blob/main/examples/README.md).*
13
15
 
14
16
  A dashboard that lives in a file gets the workflow code already has: review it
15
- in a pull request, rebuild it identically, diff it against what is live, and
16
- bring it back after a bad change. Write the spec yourself or ask an AI for one;
17
- either way, every dataset, column, and metric the spec names is confirmed to
18
- exist before anything is built, and every chart is checked to load with data
19
- after.
17
+ in a pull request, rebuild it identically, and bring it back after a bad
18
+ change. `chartwright plan` diffs the file against the live dashboard and exits
19
+ non-zero when they differ, so drift fails a CI check. `chartwright decompile`
20
+ covers the other direction, and lists anything it could not carry over.
21
+
22
+ Write the file yourself, or describe the dashboard you want and have an AI
23
+ write it. The same checks run either way. After the build, each chart's query
24
+ runs once (an error fails the build, an empty chart is named), and
25
+ `chartwright advise` reviews the layout for readability.
20
26
 
21
27
  ## What a spec looks like
22
28
 
29
+ The file is called a spec:
30
+
23
31
  ```json
24
32
  {
25
33
  "spec_version": "1",
@@ -46,8 +54,24 @@ after.
46
54
  `chartwright apply` on this file signs in to your Superset, confirms the
47
55
  `cleaned_sales_data` table and the `sales` and `order_date` columns exist,
48
56
  builds a KPI card above a line chart, and finishes by running each chart's
49
- query once to prove it shows data. A misspelled column or a missing table is a
50
- clear error naming the problem, before anything is created.
57
+ query once. A misspelled column or a missing table is a clear error naming the
58
+ problem, with the closest real names suggested, before anything is created.
59
+
60
+ ## Creating dashboards with AI
61
+
62
+ The spec is typed, and every reference in it is checked before anything is
63
+ created. Whatever a model proposes has to survive the same verification your
64
+ own specs do.
65
+
66
+ - **Claude Code skill**: from a clone of this repo, run
67
+ `python install-skill.py`; the model writes the spec from your request, and
68
+ the tool verifies and builds it.
69
+ - **MCP server**: `chartwright-mcp` (installed with `pip install "chartwright[mcp]"`)
70
+ exposes ten tools covering the whole lifecycle, usable from any MCP client.
71
+ - **Open contract**: `chartwright schema` prints the spec's JSON Schema, so any LLM or
72
+ tool can generate valid specs.
73
+ - **Guardrails**: the AI proposes; the tool verifies, using your own Superset
74
+ login. Verification reads names (datasets, columns, metrics), not rows.
51
75
 
52
76
  ## Quick start
53
77
 
@@ -84,35 +108,47 @@ as-is.
84
108
  - **Deterministic dashboards**: the same spec always produces the identical
85
109
  dashboard. Diff it in git, review it in a PR.
86
110
  - **Verified at every step**: every reference is checked before anything is
87
- written, and every chart's query runs once to prove it shows data; failures
88
- say what went wrong and where.
111
+ written, and every chart's query runs once (an error fails the build, an
112
+ empty chart is named); failures say what went wrong and where.
89
113
  - **Dashboards as code, in both directions**: `chartwright decompile` turns any live
90
114
  dashboard into a spec; `chartwright plan` shows what differs between the spec and
91
115
  the live dashboard, ready as a CI gate; `chartwright compile` builds the import
92
116
  bundle offline, no server needed.
93
- - **Safety built in**: every apply backs up the previous state first; a failed
94
- apply restores it automatically; the tool only ever overwrites dashboards it
95
- created, and a hand-built dashboard is adopted by decompiling it into a spec
96
- first.
97
- - **The full design surface**: 14 chart types, metrics as you write them,
98
- per-chart filters, a native filter bar, tabs, markdown notes, and layouts
99
- you can draw as ASCII sketches.
117
+ - **Recoverable by default**: every apply backs up the previous state first,
118
+ and an apply that fails while importing or updating charts restores it
119
+ automatically. Dashboards Chartwright did not create are never overwritten;
120
+ to bring a hand-built one under a spec, decompile it and build it at a new
121
+ slug.
122
+ - **What a spec can express**: 15 chart types, metrics as you write them,
123
+ per-chart filters, a native filter bar, tabs, markdown notes, a footer shown
124
+ under every tab, and layouts you can draw as ASCII sketches.
100
125
  - **Environment promotion**: specs name their data (connection, schema,
101
- table), so the same file applies to dev, staging, and production unchanged.
126
+ table), so the same file applies to dev, staging, and production when they
127
+ share connection names.
102
128
 
103
129
  Every capability, with the CLI verb reference: [docs/FEATURES.md](https://github.com/debabsah/chartwright/blob/main/docs/FEATURES.md).
104
130
 
105
- ## Creating dashboards with AI
131
+ ## The design brain
106
132
 
107
- - **Claude Code skill**: from a clone of this repo, `python install-skill.py`,
108
- then ask for a dashboard in plain words; the AI writes the spec, and the
109
- tool verifies and builds it.
110
- - **MCP server**: `chartwright-mcp` (installed with `pip install "chartwright[mcp]"`)
111
- exposes six tools covering the whole lifecycle, usable from any MCP client.
112
- - **Open contract**: `chartwright schema` prints the spec's JSON Schema, so any LLM or
113
- tool can generate valid specs.
114
- - **Guardrails**: the AI proposes; the tool verifies, using your own Superset
115
- login. Verification reads names (datasets, columns, metrics), not rows.
133
+ A spec can import perfectly and still produce a dashboard nobody can read: a
134
+ pie squeezed into two columns, a KPI buried under three tables, a bar chart
135
+ with forty category labels Superset silently drops. The design brain covers
136
+ size and scroll budgets, chart-choice limits, and layout composition. It
137
+ arrives as a brief the AI reads before authoring, and a critic that reviews
138
+ the result:
139
+
140
+ ```bash
141
+ chartwright brief --audience executive # the guidance, before writing a spec
142
+ chartwright advise spec.json --fix # the critique, with safe auto-repairs
143
+ chartwright redesign old-dash --profile prod # audit + repair a live dashboard
144
+ ```
145
+
146
+ You stay in charge of it: suppress any rule for one chart in the spec, tune
147
+ the thresholds per audience or per deployment in `design.yaml`, and feed the
148
+ heights you drag in the UI back into the recommendations with `chartwright
149
+ calibrate`. Pass `--design off` and the output is byte-identical to a build
150
+ that never had it. The full rulebook and architecture:
151
+ [docs/DESIGN-BRAIN.md](https://github.com/debabsah/chartwright/blob/main/docs/DESIGN-BRAIN.md).
116
152
 
117
153
  ## Drawing layouts as text
118
154
 
@@ -133,15 +169,19 @@ space under `K` deliberately empty. Every rule, drawn and explained:
133
169
 
134
170
  ## Testing and evidence
135
171
 
136
- Every push runs 125 tests on Linux and Windows, plus the full pipeline (apply,
137
- lifecycle soak, stale-tab adversary, fault injection) against real Superset
138
- 4.1.4, 5.0.0, and 6.1.0 containers. Chart options are checked against
139
- Superset's own source for every supported version, so a Superset change is
140
- caught in our tests before it reaches your dashboards.
172
+ Every pull request and every push to main runs the full offline suite on Linux and
173
+ Windows, plus the full pipeline (apply, lifecycle soak, stale-tab adversary,
174
+ fault injection) against real Superset 4.1.4, 5.0.0, and 6.1.0 containers.
175
+ Chart options are checked against Superset's own source for every supported
176
+ version, so a Superset change surfaces here before it reaches your dashboards.
177
+
178
+ Full evidence: [docs/VERIFICATION.md](https://github.com/debabsah/chartwright/blob/main/docs/VERIFICATION.md).
141
179
 
142
- Full evidence: [docs/VERIFICATION.md](https://github.com/debabsah/chartwright/blob/main/docs/VERIFICATION.md). Source citations
143
- for every Superset behavior the tool relies on:
144
- [docs/CONTRACTS.md](https://github.com/debabsah/chartwright/blob/main/docs/CONTRACTS.md).
180
+ [docs/CONTRACTS.md](https://github.com/debabsah/chartwright/blob/main/docs/CONTRACTS.md) records how Superset itself behaves: what its
181
+ importer accepts, how dashboard settings are stored, what its chart plugins
182
+ expect. Every behavior is cited to its source line at 4.1.4, 5.0.0, and 6.1.0.
183
+ Release tags are immutable, so any line of it can be checked against a fresh
184
+ checkout of that tag.
145
185
 
146
186
  ## Documentation
147
187
 
@@ -149,10 +189,15 @@ for every Superset behavior the tool relies on:
149
189
  reference
150
190
  - [docs/LAYOUT-GUIDE.md](https://github.com/debabsah/chartwright/blob/main/docs/LAYOUT-GUIDE.md): drawing layouts as text,
151
191
  every rule illustrated
192
+ - [docs/DESIGN-BRAIN.md](https://github.com/debabsah/chartwright/blob/main/docs/DESIGN-BRAIN.md): the design rulebook, every
193
+ rule and threshold, and how to tune or switch them off
152
194
  - [docs/VERIFICATION.md](https://github.com/debabsah/chartwright/blob/main/docs/VERIFICATION.md): what is tested, what it
153
195
  caught, and how to reproduce it
154
- - [docs/CONTRACTS.md](https://github.com/debabsah/chartwright/blob/main/docs/CONTRACTS.md): the Superset behaviors the tool
155
- depends on, cited to source at each supported version
196
+ - [docs/CONTRACTS.md](https://github.com/debabsah/chartwright/blob/main/docs/CONTRACTS.md): how Superset behaves on import,
197
+ on dashboard storage, and in chart options, cited to its source line at
198
+ each supported release
199
+ - [docs/COMPARISON.md](https://github.com/debabsah/chartwright/blob/main/docs/COMPARISON.md): what Chartwright, preset-cli,
200
+ sup, and the Terraform provider each automate, so you can pick by the job
156
201
 
157
202
  ---
158
203
 
@@ -4,4 +4,4 @@ Typed spec -> deterministic compiler -> guaranteed-correct Superset dashboard.
4
4
  The LLM is an untrusted parser at the edge; the guarantee lives in typed code.
5
5
  """
6
6
 
7
- __version__ = "0.1.0"
7
+ __version__ = "0.2.0"
@@ -19,6 +19,7 @@ polished geometry round-trips EXACTLY through the next apply.
19
19
  from __future__ import annotations
20
20
 
21
21
  import copy
22
+ import json
22
23
  from dataclasses import asdict, dataclass, field
23
24
 
24
25
  from . import ids
@@ -151,9 +151,14 @@ def _roundtrip_dataset_files(resolution: Resolution, client: SupersetClient) ->
151
151
  return extra
152
152
 
153
153
 
154
- def chart_payloads_from_bundle(bundle: bytes) -> dict[str, dict]:
154
+ def chart_payloads_from_bundle(bundle: bytes, dataset_ids: dict[str, int] | None = None) -> dict[str, dict]:
155
155
  """uuid -> ChartRestApi.put payload, from the compiled bundle's chart yamls.
156
156
 
157
+ With ``dataset_ids`` (dataset uuid -> id on the target), the payload also moves the
158
+ chart onto its spec dataset: params alone carry the new datasource, but the chart's
159
+ own datasource_id would stay on the old one (observed live 2026-09-25: a chart moved
160
+ from dataset 5 to 10 kept datasource_id 5).
161
+
157
162
  Used to update pre-existing owned charts IN PLACE. Slice ids must stay
158
163
  stable across re-applies: delete+reimport mints new ids, which invalidates
159
164
  native-filter scopes the moment any open browser tab writes its (stale,
@@ -164,24 +169,48 @@ def chart_payloads_from_bundle(bundle: bytes) -> dict[str, dict]:
164
169
  for n in zf.namelist():
165
170
  if "/charts/" in n and n.endswith(".yaml"):
166
171
  cy = yaml.safe_load(zf.read(n))
167
- out[str(cy.get("uuid"))] = {
172
+ payload = {
168
173
  "slice_name": cy.get("slice_name"),
169
174
  "viz_type": cy.get("viz_type"),
170
175
  "params": json.dumps(cy.get("params") or {}),
171
176
  # params changed -> any stored query context is stale
172
177
  "query_context": None,
173
178
  }
179
+ ds_id = (dataset_ids or {}).get(str(cy.get("dataset_uuid")))
180
+ if ds_id is not None:
181
+ payload["datasource_id"] = ds_id
182
+ payload["datasource_type"] = "table"
183
+ out[str(cy.get("uuid"))] = payload
174
184
  return out
175
185
 
176
186
 
187
+ def bundle_dataset_ids(bundle: bytes, client: SupersetClient) -> dict[str, int]:
188
+ """Dataset uuid -> id on the target, for each dataset the bundle ships. Looked
189
+ up per table name (indexed server-side) and matched by uuid, as charts are."""
190
+ zf = zipfile.ZipFile(io.BytesIO(bundle))
191
+ wanted: dict[str, str] = {}
192
+ for n in zf.namelist():
193
+ if "/datasets/" in n and n.endswith(".yaml"):
194
+ dy = yaml.safe_load(zf.read(n)) or {}
195
+ if dy.get("uuid") and dy.get("table_name"):
196
+ wanted[str(dy["uuid"])] = dy["table_name"]
197
+ found: dict[str, int] = {}
198
+ for table in set(wanted.values()):
199
+ for d in client.find_datasets(table):
200
+ if str(d.get("uuid")) in wanted:
201
+ found[str(d["uuid"])] = d["id"]
202
+ return found
203
+
204
+
177
205
  def _update_owned_charts_in_place(
178
206
  spec: DashboardSpec, client: SupersetClient, bundle: bytes,
179
- existing_by_uuid: dict[str, dict],
207
+ existing_by_uuid: dict[str, dict], resolution: Resolution | None = None,
180
208
  ) -> tuple[list[str], list[str]]:
181
- """PUT compiled params onto pre-existing owned charts (ids stay stable).
182
- Returns (updated chart names, errors)."""
209
+ """PUT compiled params (and the spec dataset) onto pre-existing owned charts (ids
210
+ stay stable). Returns (updated chart names, errors)."""
183
211
  owned = {str(ids.chart_uuid(spec.dashboard.slug, c.name)): c.name for c in spec.charts}
184
- payloads = chart_payloads_from_bundle(bundle)
212
+ dataset_ids = {str(d.uuid): d.id for d in resolution.datasets.values()} if resolution else None
213
+ payloads = chart_payloads_from_bundle(bundle, dataset_ids)
185
214
  updated, errors = [], []
186
215
  for u, summary in existing_by_uuid.items():
187
216
  payload = payloads.get(u)
@@ -209,6 +238,26 @@ def backup_dir_for(profile: str, slug: str) -> "Path":
209
238
  return base / profile / slug
210
239
 
211
240
 
241
+ def write_backup(backup_dir: "Path", data: bytes) -> "Path":
242
+ """Write a backup zip under a new name; never overwrite an earlier one.
243
+
244
+ Names are local time to the microsecond (``20261003T141502.123456.zip``),
245
+ fixed width, so they sort oldest to newest. Two applies within one second
246
+ used to share a name and the second overwrote the first. Exclusive create
247
+ makes a collision a retry, never a silent overwrite."""
248
+ import datetime
249
+
250
+ while True:
251
+ stamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%S.%f")
252
+ path = backup_dir / f"{stamp}.zip"
253
+ try:
254
+ with open(path, "xb") as fh:
255
+ fh.write(data)
256
+ return path
257
+ except FileExistsError:
258
+ continue
259
+
260
+
212
261
  def restore_bundle(zip_bytes: bytes, slug: str, client: SupersetClient) -> ApplyReport:
213
262
  """Restore a backup bundle COMPLETELY, not just import it. The importer
214
263
  never overwrites existing charts (docs/CONTRACTS.md), so surviving
@@ -223,7 +272,15 @@ def restore_bundle(zip_bytes: bytes, slug: str, client: SupersetClient) -> Apply
223
272
  report.import_detail = r.text[:2000]
224
273
  return report
225
274
 
226
- payloads = chart_payloads_from_bundle(zip_bytes)
275
+ # With the dataset ids, a restore also moves each chart back onto its
276
+ # backed-up dataset, so its datasource_id matches the params it gets.
277
+ # Best effort: a failed lookup must not stop the restore itself.
278
+ try:
279
+ dataset_ids = bundle_dataset_ids(zip_bytes, client)
280
+ except SupersetAPIError as e:
281
+ dataset_ids = {}
282
+ report.warnings.append(f"chart datasets not restored (dataset lookup failed: {e})")
283
+ payloads = chart_payloads_from_bundle(zip_bytes, dataset_ids)
227
284
  existing = client.charts_by_uuids(
228
285
  {u: p["slice_name"] for u, p in payloads.items()})
229
286
  restored = []
@@ -284,7 +341,6 @@ def apply(spec: DashboardSpec, client: SupersetClient, profile: str = "default")
284
341
  if existing is not None:
285
342
  # Last-known-good insurance before we mutate anything: the previous
286
343
  # owned state, restorable with `chartwright restore <zip> --profile ...`.
287
- import datetime
288
344
  import os
289
345
 
290
346
  backup_dir = backup_dir_for(profile, spec.dashboard.slug)
@@ -293,11 +349,8 @@ def apply(spec: DashboardSpec, client: SupersetClient, profile: str = "default")
293
349
  # Zips hold dashboard/dataset metadata; gate the default tree to
294
350
  # the owner. (No-op on Windows; custom dirs are the user's to manage.)
295
351
  os.chmod(backup_dir.parent.parent, 0o700)
296
- stamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%S")
297
- backup_path = backup_dir / f"{stamp}.zip"
298
352
  backup_bytes = client.export_dashboard(existing["id"])
299
- backup_path.write_bytes(backup_bytes)
300
- report.backup = str(backup_path)
353
+ report.backup = str(write_backup(backup_dir, backup_bytes))
301
354
 
302
355
  def _auto_restore(reason: str) -> None:
303
356
  """Import failed mid-mutation: put the previous state back rather
@@ -312,11 +365,11 @@ def apply(spec: DashboardSpec, client: SupersetClient, profile: str = "default")
312
365
  else:
313
366
  report.warnings.append(
314
367
  f"{reason}; auto-restore also failed ({rr.import_detail}); "
315
- f"restore manually: chartwright restore {report.backup}"
368
+ f"restore manually: chartwright restore {report.backup} --profile {profile}"
316
369
  )
317
370
  except Exception as e: # noqa: BLE001 - restore is best-effort recovery
318
371
  report.warnings.append(
319
- f"{reason}; auto-restore errored ({e}); restore manually: chartwright restore {report.backup}"
372
+ f"{reason}; auto-restore errored ({e}); restore manually: chartwright restore {report.backup} --profile {profile}"
320
373
  )
321
374
 
322
375
  # Everything below mutates the instance; any failure must still return a
@@ -359,9 +412,14 @@ def apply(spec: DashboardSpec, client: SupersetClient, profile: str = "default")
359
412
  return report
360
413
 
361
414
  if existing_by_uuid:
362
- updated, update_errors = _update_owned_charts_in_place(spec, client, bundle, existing_by_uuid)
415
+ updated, update_errors = _update_owned_charts_in_place(
416
+ spec, client, bundle, existing_by_uuid, resolution)
363
417
  if update_errors:
418
+ # Still the import step: some charts may carry the new params
419
+ # and others the old. Same outcome as a connection drop here,
420
+ # which already restored (SupersetAPIError while stage is import).
364
421
  report.import_detail = "; ".join(update_errors)
422
+ _auto_restore("updating charts in place failed after the import")
365
423
  return report
366
424
  if updated:
367
425
  report.warnings.append(
@@ -403,6 +461,13 @@ def apply(spec: DashboardSpec, client: SupersetClient, profile: str = "default")
403
461
  if report.stage in ("prepare", "import"):
404
462
  _auto_restore(f"apply failed during {report.stage}: {e}")
405
463
  return report
464
+ except Exception as e: # noqa: BLE001 - a bug mid-mutation must still restore and report
465
+ # Not a Superset error: most likely a bug here. Same rule as above, and
466
+ # the report keeps the backup path, which a traceback would lose.
467
+ report.import_detail = f"unexpected error during {report.stage}: {type(e).__name__}: {e}"
468
+ if report.stage in ("prepare", "import"):
469
+ _auto_restore(f"apply failed during {report.stage}: {type(e).__name__}: {e}")
470
+ return report
406
471
 
407
472
  report.stage = "done"
408
473
  report.ok = True