trellum 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 (313) hide show
  1. trellum-0.2.0/AGENTS.md +671 -0
  2. trellum-0.2.0/LICENSE +22 -0
  3. trellum-0.2.0/PKG-INFO +1603 -0
  4. trellum-0.2.0/README.md +1534 -0
  5. trellum-0.2.0/__init__.py +14 -0
  6. trellum-0.2.0/__main__.py +21 -0
  7. trellum-0.2.0/agent/__init__.py +18 -0
  8. trellum-0.2.0/agent/agentdoc.py +311 -0
  9. trellum-0.2.0/agent/agentsetup.py +294 -0
  10. trellum-0.2.0/assets.py +69 -0
  11. trellum-0.2.0/cli/__init__.py +167 -0
  12. trellum-0.2.0/cli/commands/__init__.py +6 -0
  13. trellum-0.2.0/cli/commands/data.py +242 -0
  14. trellum-0.2.0/cli/commands/doctor.py +135 -0
  15. trellum-0.2.0/cli/commands/guide.py +158 -0
  16. trellum-0.2.0/cli/commands/review.py +306 -0
  17. trellum-0.2.0/cli/commands/serve.py +130 -0
  18. trellum-0.2.0/cli/commands/setup.py +107 -0
  19. trellum-0.2.0/cli/commands/validate.py +43 -0
  20. trellum-0.2.0/cli/console.py +22 -0
  21. trellum-0.2.0/cli/describe.py +91 -0
  22. trellum-0.2.0/components/__init__.py +50 -0
  23. trellum-0.2.0/components/ab_compare.py +406 -0
  24. trellum-0.2.0/components/ab_methodology.py +113 -0
  25. trellum-0.2.0/components/base.py +202 -0
  26. trellum-0.2.0/components/charts/__init__.py +26 -0
  27. trellum-0.2.0/components/charts/bar.py +411 -0
  28. trellum-0.2.0/components/charts/base.py +120 -0
  29. trellum-0.2.0/components/charts/doughnut.py +89 -0
  30. trellum-0.2.0/components/charts/funnel.py +73 -0
  31. trellum-0.2.0/components/charts/heatmap.py +139 -0
  32. trellum-0.2.0/components/charts/line_area.py +254 -0
  33. trellum-0.2.0/components/charts/scatter.py +119 -0
  34. trellum-0.2.0/components/charts/treemap.py +83 -0
  35. trellum-0.2.0/components/controls.py +160 -0
  36. trellum-0.2.0/components/filterable.py +419 -0
  37. trellum-0.2.0/components/filters/__init__.py +44 -0
  38. trellum-0.2.0/components/filters/base.py +53 -0
  39. trellum-0.2.0/components/filters/date_range.py +196 -0
  40. trellum-0.2.0/components/filters/dropdown.py +63 -0
  41. trellum-0.2.0/components/filters/flag.py +38 -0
  42. trellum-0.2.0/components/filters/toggle.py +56 -0
  43. trellum-0.2.0/components/header.py +119 -0
  44. trellum-0.2.0/components/kpis.py +194 -0
  45. trellum-0.2.0/components/layout.py +234 -0
  46. trellum-0.2.0/components/tables.py +251 -0
  47. trellum-0.2.0/config.yaml +17 -0
  48. trellum-0.2.0/data/__init__.py +32 -0
  49. trellum-0.2.0/data/connections.py +123 -0
  50. trellum-0.2.0/data/datasource_config.py +137 -0
  51. trellum-0.2.0/data/drivers/__init__.py +78 -0
  52. trellum-0.2.0/data/drivers/bigquery.py +48 -0
  53. trellum-0.2.0/data/drivers/clickhouse.py +27 -0
  54. trellum-0.2.0/data/drivers/duckdb_driver.py +25 -0
  55. trellum-0.2.0/data/drivers/mysql.py +28 -0
  56. trellum-0.2.0/data/drivers/postgres.py +27 -0
  57. trellum-0.2.0/data/drivers/redshift.py +21 -0
  58. trellum-0.2.0/data/drivers/snowflake_driver.py +35 -0
  59. trellum-0.2.0/data/drivers/sqlite.py +21 -0
  60. trellum-0.2.0/data/drivers/sqlserver.py +32 -0
  61. trellum-0.2.0/data/drivers/vertica.py +47 -0
  62. trellum-0.2.0/data/query.py +561 -0
  63. trellum-0.2.0/data/resolvers.py +232 -0
  64. trellum-0.2.0/data/transforms.py +90 -0
  65. trellum-0.2.0/demo/README.md +261 -0
  66. trellum-0.2.0/demo/__init__.py +28 -0
  67. trellum-0.2.0/demo/__main__.py +156 -0
  68. trellum-0.2.0/demo/config.yaml +0 -0
  69. trellum-0.2.0/demo/data-sources/config.yaml +16 -0
  70. trellum-0.2.0/demo/data-sources/uploads/ua_budget.csv +325 -0
  71. trellum-0.2.0/demo/reports/_template/__init__.py +0 -0
  72. trellum-0.2.0/demo/reports/_template/generator.py +72 -0
  73. trellum-0.2.0/demo/reports/_template/queries.py +20 -0
  74. trellum-0.2.0/demo/reports/_template/report.yaml +34 -0
  75. trellum-0.2.0/demo/reports/cart-funnel/__init__.py +0 -0
  76. trellum-0.2.0/demo/reports/cart-funnel/custom_sections.py +326 -0
  77. trellum-0.2.0/demo/reports/cart-funnel/generator.py +159 -0
  78. trellum-0.2.0/demo/reports/cart-funnel/queries.py +22 -0
  79. trellum-0.2.0/demo/reports/cart-funnel/report.yaml +39 -0
  80. trellum-0.2.0/demo/reports/checkout-experiment/__init__.py +0 -0
  81. trellum-0.2.0/demo/reports/checkout-experiment/generator.py +113 -0
  82. trellum-0.2.0/demo/reports/checkout-experiment/queries.py +21 -0
  83. trellum-0.2.0/demo/reports/checkout-experiment/report.yaml +37 -0
  84. trellum-0.2.0/demo/reports/conversion/__init__.py +0 -0
  85. trellum-0.2.0/demo/reports/conversion/custom_sections.py +164 -0
  86. trellum-0.2.0/demo/reports/conversion/generator.py +174 -0
  87. trellum-0.2.0/demo/reports/conversion/queries.py +13 -0
  88. trellum-0.2.0/demo/reports/conversion/report.yaml +58 -0
  89. trellum-0.2.0/demo/reports/economy-firehose/__init__.py +0 -0
  90. trellum-0.2.0/demo/reports/economy-firehose/generator.py +227 -0
  91. trellum-0.2.0/demo/reports/economy-firehose/queries.py +21 -0
  92. trellum-0.2.0/demo/reports/economy-firehose/report.yaml +51 -0
  93. trellum-0.2.0/demo/reports/insert-coin/__init__.py +0 -0
  94. trellum-0.2.0/demo/reports/insert-coin/custom_sections.py +750 -0
  95. trellum-0.2.0/demo/reports/insert-coin/generator.py +165 -0
  96. trellum-0.2.0/demo/reports/insert-coin/queries.py +21 -0
  97. trellum-0.2.0/demo/reports/insert-coin/report.yaml +43 -0
  98. trellum-0.2.0/demo/reports/monetization/__init__.py +0 -0
  99. trellum-0.2.0/demo/reports/monetization/custom_sections.py +486 -0
  100. trellum-0.2.0/demo/reports/monetization/generator.py +242 -0
  101. trellum-0.2.0/demo/reports/monetization/queries.py +56 -0
  102. trellum-0.2.0/demo/reports/monetization/report.yaml +34 -0
  103. trellum-0.2.0/demo/reports/player-overview/__init__.py +0 -0
  104. trellum-0.2.0/demo/reports/player-overview/custom_sections.py +451 -0
  105. trellum-0.2.0/demo/reports/player-overview/generator.py +283 -0
  106. trellum-0.2.0/demo/reports/player-overview/queries.py +49 -0
  107. trellum-0.2.0/demo/reports/player-overview/report.yaml +49 -0
  108. trellum-0.2.0/demo/reports/store-health/__init__.py +0 -0
  109. trellum-0.2.0/demo/reports/store-health/generator.py +225 -0
  110. trellum-0.2.0/demo/reports/store-health/queries.py +23 -0
  111. trellum-0.2.0/demo/reports/store-health/report.yaml +41 -0
  112. trellum-0.2.0/demo/tools/__init__.py +1 -0
  113. trellum-0.2.0/demo/tools/list_reports.py +130 -0
  114. trellum-0.2.0/demo/tools/make_fixtures.py +1429 -0
  115. trellum-0.2.0/init/__init__.py +0 -0
  116. trellum-0.2.0/init/__main__.py +5 -0
  117. trellum-0.2.0/meta.py +68 -0
  118. trellum-0.2.0/new/__init__.py +0 -0
  119. trellum-0.2.0/new/__main__.py +5 -0
  120. trellum-0.2.0/output_backends/__init__.py +3 -0
  121. trellum-0.2.0/output_backends/backends.py +78 -0
  122. trellum-0.2.0/output_backends/local.py +39 -0
  123. trellum-0.2.0/project.py +157 -0
  124. trellum-0.2.0/pyproject.toml +162 -0
  125. trellum-0.2.0/rendering/__init__.py +5 -0
  126. trellum-0.2.0/rendering/artifacts.py +162 -0
  127. trellum-0.2.0/rendering/cdn.py +268 -0
  128. trellum-0.2.0/rendering/html_builder.py +481 -0
  129. trellum-0.2.0/rendering/js_runtime.py +95 -0
  130. trellum-0.2.0/rendering/sections.py +116 -0
  131. trellum-0.2.0/report.py +403 -0
  132. trellum-0.2.0/reporting/__init__.py +18 -0
  133. trellum-0.2.0/reporting/diagnostics/__init__.py +168 -0
  134. trellum-0.2.0/reporting/diagnostics/datasets.py +121 -0
  135. trellum-0.2.0/reporting/diagnostics/filters.py +353 -0
  136. trellum-0.2.0/reporting/diagnostics/types.py +63 -0
  137. trellum-0.2.0/reporting/diagnostics/walk.py +166 -0
  138. trellum-0.2.0/reporting/gallery.py +361 -0
  139. trellum-0.2.0/review/__init__.py +32 -0
  140. trellum-0.2.0/review/http.py +185 -0
  141. trellum-0.2.0/review/inject.py +115 -0
  142. trellum-0.2.0/review/state.py +216 -0
  143. trellum-0.2.0/run/__init__.py +0 -0
  144. trellum-0.2.0/run/__main__.py +5 -0
  145. trellum-0.2.0/runner/__init__.py +349 -0
  146. trellum-0.2.0/runner/console.py +28 -0
  147. trellum-0.2.0/runner/discovery.py +177 -0
  148. trellum-0.2.0/runner/events.py +263 -0
  149. trellum-0.2.0/runner/execute.py +410 -0
  150. trellum-0.2.0/runner/ports.py +244 -0
  151. trellum-0.2.0/runner/serve.py +244 -0
  152. trellum-0.2.0/scaffold/__init__.py +16 -0
  153. trellum-0.2.0/scaffold/init_project.py +162 -0
  154. trellum-0.2.0/scaffold/new_report.py +325 -0
  155. trellum-0.2.0/setup.cfg +4 -0
  156. trellum-0.2.0/static/css/base.css +297 -0
  157. trellum-0.2.0/static/css/components/ab_compare.css +145 -0
  158. trellum-0.2.0/static/css/components/ab_methodology.css +91 -0
  159. trellum-0.2.0/static/css/components/chart_base.css +51 -0
  160. trellum-0.2.0/static/css/components/data_table.css +74 -0
  161. trellum-0.2.0/static/css/components/date_range_filter.css +63 -0
  162. trellum-0.2.0/static/css/components/filter_bar.css +165 -0
  163. trellum-0.2.0/static/css/components/grid.css +23 -0
  164. trellum-0.2.0/static/css/components/header.css +153 -0
  165. trellum-0.2.0/static/css/components/kpi_card.css +64 -0
  166. trellum-0.2.0/static/css/components/pivot_table.css +25 -0
  167. trellum-0.2.0/static/css/components/tab_group.css +51 -0
  168. trellum-0.2.0/static/css/components/toggle.css +29 -0
  169. trellum-0.2.0/static/css/review.css +82 -0
  170. trellum-0.2.0/static/js/components/ab_compare.js +190 -0
  171. trellum-0.2.0/static/js/components/chart_base.js +672 -0
  172. trellum-0.2.0/static/js/components/data_source.js +5 -0
  173. trellum-0.2.0/static/js/components/data_table.js +212 -0
  174. trellum-0.2.0/static/js/components/date_range_filter.js +201 -0
  175. trellum-0.2.0/static/js/components/doughnut.js +80 -0
  176. trellum-0.2.0/static/js/components/dropdown_filter.js +186 -0
  177. trellum-0.2.0/static/js/components/filter_bar.js +151 -0
  178. trellum-0.2.0/static/js/components/flag_filter.js +30 -0
  179. trellum-0.2.0/static/js/components/funnel.js +98 -0
  180. trellum-0.2.0/static/js/components/header.js +108 -0
  181. trellum-0.2.0/static/js/components/heatmap.js +204 -0
  182. trellum-0.2.0/static/js/components/kpi_card.js +17 -0
  183. trellum-0.2.0/static/js/components/kpi_row.js +82 -0
  184. trellum-0.2.0/static/js/components/pivot_table.js +206 -0
  185. trellum-0.2.0/static/js/components/scatter.js +116 -0
  186. trellum-0.2.0/static/js/components/scoped_data_source.js +3 -0
  187. trellum-0.2.0/static/js/components/toggle_filter.js +35 -0
  188. trellum-0.2.0/static/js/components/treemap.js +140 -0
  189. trellum-0.2.0/static/js/data_loader.js +452 -0
  190. trellum-0.2.0/static/js/review/chart_target.js +180 -0
  191. trellum-0.2.0/static/js/review/dom.js +68 -0
  192. trellum-0.2.0/static/js/review/end_session.js +25 -0
  193. trellum-0.2.0/static/js/review/identify.js +71 -0
  194. trellum-0.2.0/static/js/review/keyboard.js +7 -0
  195. trellum-0.2.0/static/js/review/log.js +35 -0
  196. trellum-0.2.0/static/js/review/net.js +58 -0
  197. trellum-0.2.0/static/js/review/panel.js +13 -0
  198. trellum-0.2.0/static/js/review/popover.js +78 -0
  199. trellum-0.2.0/static/js/review/presence.js +14 -0
  200. trellum-0.2.0/static/js/review/queue.js +94 -0
  201. trellum-0.2.0/static/js/review/select.js +13 -0
  202. trellum-0.2.0/static/js/review/state.js +35 -0
  203. trellum-0.2.0/static/js/review/status.js +90 -0
  204. trellum-0.2.0/static/js/review/styles.js +6 -0
  205. trellum-0.2.0/static/js/runtime/aggregate.js +100 -0
  206. trellum-0.2.0/static/js/runtime/annotations.js +286 -0
  207. trellum-0.2.0/static/js/runtime/auto_refresh.js +45 -0
  208. trellum-0.2.0/static/js/runtime/autofill.js +40 -0
  209. trellum-0.2.0/static/js/runtime/chart_defaults.js +316 -0
  210. trellum-0.2.0/static/js/runtime/chunk_loader.js +184 -0
  211. trellum-0.2.0/static/js/runtime/color_registry.js +9 -0
  212. trellum-0.2.0/static/js/runtime/cross_filter.js +21 -0
  213. trellum-0.2.0/static/js/runtime/csv_download.js +18 -0
  214. trellum-0.2.0/static/js/runtime/export.js +62 -0
  215. trellum-0.2.0/static/js/runtime/filter_engine.js +460 -0
  216. trellum-0.2.0/static/js/runtime/formatters.js +52 -0
  217. trellum-0.2.0/static/js/runtime/fw_namespace.js +37 -0
  218. trellum-0.2.0/static/js/runtime/live_wrap.js +10 -0
  219. trellum-0.2.0/static/js/runtime/state.js +82 -0
  220. trellum-0.2.0/static/js/runtime/storage.js +5 -0
  221. trellum-0.2.0/static/js/runtime/theme.js +136 -0
  222. trellum-0.2.0/static/js/runtime/ui_handlers.js +152 -0
  223. trellum-0.2.0/static/js/runtime/url_sync.js +249 -0
  224. trellum-0.2.0/static/vendor/MANIFEST.json +140 -0
  225. trellum-0.2.0/static/vendor/UPDATING.md +64 -0
  226. trellum-0.2.0/static/vendor/chart.umd.min.js +14 -0
  227. trellum-0.2.0/static/vendor/chartjs-chart-funnel.umd.min.js +16 -0
  228. trellum-0.2.0/static/vendor/chartjs-chart-geo.umd.min.js +2 -0
  229. trellum-0.2.0/static/vendor/chartjs-chart-matrix.min.js +8 -0
  230. trellum-0.2.0/static/vendor/chartjs-chart-sankey.min.js +7 -0
  231. trellum-0.2.0/static/vendor/chartjs-chart-treemap.min.js +8 -0
  232. trellum-0.2.0/static/vendor/chartjs-chart-venn.umd.min.js +2 -0
  233. trellum-0.2.0/static/vendor/chartjs-plugin-annotation.min.js +7 -0
  234. trellum-0.2.0/static/vendor/chartjs-plugin-datalabels.min.js +7 -0
  235. trellum-0.2.0/static/vendor/chartjs-plugin-zoom.min.js +7 -0
  236. trellum-0.2.0/static/vendor/countries-110m.json +1 -0
  237. trellum-0.2.0/static/vendor/fonts/inter-400.ttf +0 -0
  238. trellum-0.2.0/static/vendor/fonts/inter-500.ttf +0 -0
  239. trellum-0.2.0/static/vendor/fonts/inter-600.ttf +0 -0
  240. trellum-0.2.0/static/vendor/fonts/inter-700.ttf +0 -0
  241. trellum-0.2.0/static/vendor/hammer.min.js +7 -0
  242. trellum-0.2.0/static/vendor/html2canvas.min.js +20 -0
  243. trellum-0.2.0/static/vendor/inter.css +31 -0
  244. trellum-0.2.0/static/vendor/jspdf.umd.min.js +398 -0
  245. trellum-0.2.0/static/vendor/nouislider.min.css +1 -0
  246. trellum-0.2.0/static/vendor/nouislider.min.js +1 -0
  247. trellum-0.2.0/static/vendor/plotly.min.js +8 -0
  248. trellum-0.2.0/static/vendor/slimselect.css +1 -0
  249. trellum-0.2.0/static/vendor/slimselect.min.js +1 -0
  250. trellum-0.2.0/static/vendor/topojson-client.min.js +2 -0
  251. trellum-0.2.0/stats/__init__.py +75 -0
  252. trellum-0.2.0/stats/ab/__init__.py +64 -0
  253. trellum-0.2.0/stats/ab/aggregate.py +250 -0
  254. trellum-0.2.0/stats/ab/modes.py +504 -0
  255. trellum-0.2.0/stats/ab/users.py +292 -0
  256. trellum-0.2.0/stats/bootstrap.py +86 -0
  257. trellum-0.2.0/stats/cuped.py +78 -0
  258. trellum-0.2.0/stats/winsor.py +34 -0
  259. trellum-0.2.0/testing/__init__.py +14 -0
  260. trellum-0.2.0/testing/conftest.py +86 -0
  261. trellum-0.2.0/testing/mock_data.py +292 -0
  262. trellum-0.2.0/testing/runner.py +711 -0
  263. trellum-0.2.0/testing/test_ab_frontdoor.py +208 -0
  264. trellum-0.2.0/testing/test_agentdoc.py +490 -0
  265. trellum-0.2.0/testing/test_cli_query.py +153 -0
  266. trellum-0.2.0/testing/test_compatibility.py +466 -0
  267. trellum-0.2.0/testing/test_components.py +745 -0
  268. trellum-0.2.0/testing/test_connections.py +1306 -0
  269. trellum-0.2.0/testing/test_demo_journey.py +124 -0
  270. trellum-0.2.0/testing/test_gallery.py +189 -0
  271. trellum-0.2.0/testing/test_interactions.py +467 -0
  272. trellum-0.2.0/testing/test_js_runtime.py +820 -0
  273. trellum-0.2.0/testing/test_module_size.py +150 -0
  274. trellum-0.2.0/testing/test_performance.py +797 -0
  275. trellum-0.2.0/testing/test_portable_output.py +95 -0
  276. trellum-0.2.0/testing/test_review.py +801 -0
  277. trellum-0.2.0/testing/test_stats.py +383 -0
  278. trellum-0.2.0/testing/test_validation.py +354 -0
  279. trellum-0.2.0/testing/visual_regression.py +161 -0
  280. trellum-0.2.0/themes/__init__.py +76 -0
  281. trellum-0.2.0/themes/blossom.py +35 -0
  282. trellum-0.2.0/themes/classic.py +74 -0
  283. trellum-0.2.0/themes/dark.py +36 -0
  284. trellum-0.2.0/themes/default.py +3 -0
  285. trellum-0.2.0/themes/dracula.py +35 -0
  286. trellum-0.2.0/themes/midnight.py +35 -0
  287. trellum-0.2.0/themes/money.py +35 -0
  288. trellum-0.2.0/themes/monokai.py +35 -0
  289. trellum-0.2.0/themes/nord.py +35 -0
  290. trellum-0.2.0/themes/ocean.py +35 -0
  291. trellum-0.2.0/themes/solarized.py +35 -0
  292. trellum-0.2.0/themes/sunset.py +35 -0
  293. trellum-0.2.0/themes/theme.py +85 -0
  294. trellum-0.2.0/trellum.egg-info/PKG-INFO +1603 -0
  295. trellum-0.2.0/trellum.egg-info/SOURCES.txt +614 -0
  296. trellum-0.2.0/trellum.egg-info/dependency_links.txt +1 -0
  297. trellum-0.2.0/trellum.egg-info/entry_points.txt +2 -0
  298. trellum-0.2.0/trellum.egg-info/requires.txt +20 -0
  299. trellum-0.2.0/trellum.egg-info/top_level.txt +1 -0
  300. trellum-0.2.0/validation/__init__.py +123 -0
  301. trellum-0.2.0/validation/checks/__init__.py +6 -0
  302. trellum-0.2.0/validation/checks/annotations.py +113 -0
  303. trellum-0.2.0/validation/checks/columns.py +387 -0
  304. trellum-0.2.0/validation/checks/datasource.py +357 -0
  305. trellum-0.2.0/validation/checks/effectiveness.py +157 -0
  306. trellum-0.2.0/validation/checks/rawhtml.py +195 -0
  307. trellum-0.2.0/validation/checks/scopes.py +28 -0
  308. trellum-0.2.0/validation/checks/structural.py +87 -0
  309. trellum-0.2.0/validation/checks/theme.py +159 -0
  310. trellum-0.2.0/validation/checks/visibility.py +121 -0
  311. trellum-0.2.0/validation/checks/yaml_schema.py +50 -0
  312. trellum-0.2.0/validation/result.py +214 -0
  313. trellum-0.2.0/validation/walk.py +138 -0
@@ -0,0 +1,671 @@
1
+ # trellum -- Agent Workflow Guide
2
+
3
+ This is the **workflow guide** for AI assistants (Claude Code, Cursor, Codex)
4
+ building reports with this framework. It is the only document you need resident.
5
+
6
+ `README.md` is written for people, and it is long. **Do not load it wholesale.**
7
+ Ask for the part you need instead:
8
+
9
+ ```
10
+ trellum what the framework is, in ~30 lines
11
+ trellum guide <topic> one section of depth, on demand
12
+ trellum checks every validator check id and its level
13
+ trellum validate <dir> the current validation state of a built report
14
+ ```
15
+
16
+ `trellum guide` topics: `queries`, `format`, `generator`, `components`,
17
+ `filters`, `rawhtml`, `validation`, `report-yaml`, `themes`, `review`.
18
+
19
+ **Why this shape.** Your whole context is re-read on every API round-trip, so a
20
+ large always-loaded reference is paid for dozens of times per task. A command is
21
+ paid for once, when it is actually needed. Reach for `trellum guide` freely —
22
+ it is cheaper than it looks, and far cheaper than guessing.
23
+
24
+ ## Where to look, by question
25
+
26
+ | Question | Answer |
27
+ |---|---|
28
+ | What components exist? | `trellum` — the bare command lists all of them |
29
+ | What does this validator check id mean? | `trellum checks`, then `trellum guide validation` |
30
+ | How do I filter a chart? | `trellum guide filters` |
31
+ | Why is my chart empty / not reacting? | `trellum guide format` — it is almost always wide-vs-long |
32
+ | Where does aggregation belong? | `trellum guide queries` — pandas, not SQL |
33
+ | What goes in report.yaml? | `trellum guide report-yaml` |
34
+ | Everything else | `README.md`, read the relevant section only |
35
+
36
+ **Before writing a generator, read `trellum guide queries` and
37
+ `trellum guide format`.** Those two rules are not guessable from the API, and
38
+ getting them wrong produces a report that builds cleanly and displays nothing
39
+ useful.
40
+
41
+ ## When to use the framework
42
+
43
+ | Goal | Where it goes |
44
+ |---|---|
45
+ | Recurring, interactive report — served standalone, on a schedule, or embedded in another application | `reports/{slug}/` (the framework) |
46
+ | One-off data exploration, "what was X yesterday", quick prototyping | A plain script or notebook — the framework's structure buys nothing for throwaway analysis |
47
+
48
+ Scaffold a new report: `python3 -m trellum.new my-report --studio my-studio --category Revenue`. Build it: `python3 -m trellum.run reports/my-report --no-serve`.
49
+
50
+ **Always pass `--no-serve` unless you have been asked to open the report in a
51
+ browser.** Without it the command starts a preview server on :8050 and never
52
+ exits — the build itself finishes in under a second, but the process stays in
53
+ the foreground until it is killed. Build first, then serve as a separate step
54
+ if a human is going to look at it.
55
+
56
+ **Read files with your file-reading tool, not with `cat`, `ls` or `grep` in a
57
+ shell.** Every shell command starts a process; your native read, glob and search
58
+ tools do not. On the machine these runs were measured on a bare `ls` cost about
59
+ twelve seconds and an identical `Read` cost none — that gap is environment-
60
+ specific, but its direction never is.
61
+
62
+ The trap is that batching looks cheaper: `cat a.py; cat b.py; cat c.py` is one
63
+ command where three reads are three calls. Under a per-command cost that
64
+ reasoning is right, and here it is exactly backwards — three native reads are
65
+ free and the one shell command is not. Use the shell for things that genuinely
66
+ need it: running the build, the validator, git.
67
+
68
+ When you do serve, two things are not guessable and both have cost real time:
69
+
70
+ - **A single report is served at `/`, not at `/<slug>`.** Only `--all --serve`
71
+ puts an index at `/` with reports beneath it. `/<slug>` on a single-report
72
+ server returns an error page.
73
+ - **Wait for the `Serving at ...` line before opening the URL.** `--serve`
74
+ rebuilds the report and binds the port last, so until that line appears the
75
+ URL may still be answered by a previous server — showing a different report
76
+ rather than failing, which is far more confusing than a refused connection.
77
+
78
+ <!-- topic: queries -->
79
+ ## How to write queries (MANDATORY — read before touching `queries.py`)
80
+
81
+ **Exploring the data first?** `python -m trellum query "SELECT ..."` runs
82
+ ad-hoc SQL against a configured source by NAME — never hunt for the database
83
+ file or hardcode its path, both of which break the day the source is a
84
+ remote warehouse. `--param day=2026-06-15` binds `:day`; sqlite sources are
85
+ opened read-only.
86
+
87
+ **The rule for report queries:** SQL pulls raw rows with a date filter.
88
+ Python does the aggregation. Never both in the same query.
89
+
90
+ ```sql
91
+ -- ✓ RIGHT — minimal SQL: date filter + column projection, no aggregation
92
+ SELECT event_date, dim_a, dim_b, metric_col
93
+ FROM <schema>.<fact_table>
94
+ WHERE event_date BETWEEN DATE :start_date AND DATE :end_date
95
+ ```
96
+
97
+ ```python
98
+ # ✓ RIGHT — aggregate in the generator with pandas
99
+ df_raw = query_df(conn, queries.MY_QUERY, params={...})
100
+ daily = df_raw.groupby("event_date")["metric_col"].sum().reset_index()
101
+ split = df_raw.groupby(["event_date", "dim_a"])["metric_col"].sum().reset_index()
102
+ ```
103
+
104
+ ```sql
105
+ -- ✗ WRONG — SQL-side GROUP BY on a fact table
106
+ SELECT event_date, dim_a, SUM(metric_col)
107
+ FROM <schema>.<fact_table>
108
+ WHERE event_date BETWEEN ... GROUP BY 1, 2
109
+ ```
110
+
111
+ Two reasons this rule is non-negotiable:
112
+
113
+ 1. **Memory footprint.** A `GROUP BY + SUM` on a wide fact table makes
114
+ the database pre-allocate several GB for the hash aggregate, and
115
+ local-dev user accounts frequently run under tight resource-pool
116
+ caps. Raw-row retrieval with a date filter stays small — a typical
117
+ 30-day window of a fact table fits in tens of MB of pandas.
118
+ 2. **Dynamic filtering is the framework's whole point.** The
119
+ client-side `FilterBar` re-aggregates every time the user changes
120
+ a filter. Pre-aggregating in SQL collapses dimensions and breaks
121
+ that interactivity — the chart freezes at whatever grain the SQL
122
+ produced. Raw-row `DataSource`s let the client slice any dimension
123
+ you included.
124
+
125
+ Code smells — triggers to rewrite the query:
126
+
127
+ - `SUM()` / `AVG()` / `COUNT(DISTINCT)` in the SELECT that collapses a
128
+ dimension the user might want to filter by.
129
+ - `GROUP BY` on only a subset of the filterable dimensions. Either
130
+ drop the GROUP BY or group by every dim the client might filter on
131
+ (denormalized grain).
132
+ - `HAVING` — do it in pandas.
133
+ - `JOIN` that explodes row count to attach a dim — better as a pandas
134
+ `.merge()` after both sides load.
135
+
136
+ Acceptable exceptions (rare):
137
+
138
+ - Querying an already-aggregated table (one whose name signals a
139
+ pre-rolled grain like `*_agg_*` / `*_daily_*`): pull columns
140
+ directly, no further SQL aggregation.
141
+ - A `GROUP BY` on the union of every filterable dimension. Preserves
142
+ filterability but compresses duplicates. Use only if raw-row
143
+ retrieval genuinely returns too much data.
144
+
145
+ Project-specific table names, memory caps, and schema particulars live
146
+ in `project_context/chat_rules/` — the chat feature loads them at
147
+ runtime; human readers can look there for the concrete tables to query.
148
+
149
+ <!-- topic: format -->
150
+ ## Long format vs wide format (MANDATORY)
151
+
152
+ **Rule:** dimensions stay in **rows**, never in **column names**. One
153
+ row per (date × every breakdown dim), one column per metric.
154
+
155
+ ```python
156
+ # ✓ RIGHT — long format
157
+ event_date | platform | spender_tier | dau | iap_revenue | ad_revenue
158
+ 2026-04-01 | ios | Whale | 12000 | 9800 | 250
159
+ 2026-04-01 | android | Whale | 8500 | 5400 | 180
160
+ ```
161
+
162
+ ```python
163
+ # ✗ WRONG — platform baked into column names ("wide format")
164
+ event_date | spender_tier | dau_ios | dau_android | iap_revenue_ios | iap_revenue_android | ...
165
+ 2026-04-01 | Whale | 12000 | 8500 | 9800 | 5400 | ...
166
+ ```
167
+
168
+ Why long format is the default:
169
+
170
+ 1. **Filter coverage.** The client filter engine matches on column
171
+ *values*, not column *names*. A filter on `platform = "ios"` only
172
+ works when there's a `platform` column carrying `"ios"` as a value.
173
+ Wide format makes the filter inert.
174
+ 2. **Smaller payload.** Dictionary encoding via `_serialize_columnar`
175
+ compresses high-repetition columns (e.g. `platform` with 8 unique
176
+ values across 50 k rows) to integer indices — typically 60–70 %
177
+ smaller than the wide equivalent with one column per platform.
178
+ 3. **Auto-discovery.** Adding a new platform just adds rows to the
179
+ data; charts using `stack_by="platform"` pick it up. Wide format
180
+ forces editing every chart's `y=[col_a, col_b, ...]` list.
181
+ 4. **Simpler SQL.** `GROUP BY event_date, platform, ...` instead of
182
+ one `SUM(CASE WHEN platform='ios' THEN dau END) AS dau_ios` per
183
+ metric per platform.
184
+
185
+ Render breakdowns with `stack_by`:
186
+
187
+ ```python
188
+ # Long format → one line per platform via stack_by
189
+ LineChart(df=df, x="event_date", y="iap_revenue",
190
+ stack_by="platform", dataset_id=ds, y_format="currency",
191
+ title="IAP Revenue by Platform")
192
+
193
+ # Long format + ratio → one line per platform, computing iap/dau per (date, platform)
194
+ LineChart(df=df, x="event_date", dataset_id=ds,
195
+ ratios=[{"numerator": "iap_revenue", "denominator": "dau",
196
+ "label": "IAP ARPDAU"}],
197
+ stack_by="platform", y_format="currency",
198
+ title="ARPDAU (IAP) by Platform")
199
+ ```
200
+
201
+ The `chart-filter-coverage` validator catches accidental wide-pivots —
202
+ when a chart's DataSource cannot react to a FilterBar filter because
203
+ the dimension was collapsed into column names. Long format is the
204
+ fix; suppression is reserved for charts that are intentionally a
205
+ fixed rollup (e.g. period-over-period reference rollups).
206
+
207
+ <!-- topic: generator -->
208
+ ## The 5-step process to write generator.py
209
+
210
+ 1. Subclass `BaseReport` and implement `generate(self, ctx)`.
211
+ 2. `conn = ctx.get_connection("primary_warehouse")` (or whatever name the project uses in `data-sources/config.yaml`); query with `query_df(conn, queries.X, params={...})`.
212
+ 3. Wrap each DataFrame in a `DataSource` + a `FilterBar` placed together in an **untitled section**: `ctx.add_section("", [DataSource(...), FilterBar(...)])`. Untitled is required for sticky positioning.
213
+ 4. Add content sections: `ctx.add_section(title, [...])`.
214
+ 5. **Every chart, KPI, and table component MUST carry `dataset_id="..."`** pointing to its DataSource. Without it the component renders statically and silently ignores filters — this is the most common mistake.
215
+
216
+ To copy from, list what this project actually has (`ls reports/`) and open one.
217
+ Report names are not named here on purpose: this file ships with the framework
218
+ and travels into every project, so any slug written down is a report somebody
219
+ else has. A measured run followed three such names and got three "file does not
220
+ exist" errors before it thought to look.
221
+
222
+ <!-- topic: report-yaml -->
223
+ ## Write the report.yaml description for discoverability (MANDATORY)
224
+
225
+ A search or assistant layer routes user questions to reports using
226
+ the `description` and `tags` in `report.yaml`, plus the dataset columns it
227
+ indexes from the built output. A vague description ("Revenue report") makes
228
+ the report invisible to it. The description MUST state:
229
+
230
+ 1. **Which business questions the report answers** ("how many payers churn
231
+ per week and why"), not just its topic.
232
+ 2. **The key metrics and dimensions** it carries (churn rate, revenue at
233
+ risk; split by platform/tier/country).
234
+ 3. **Grain and freshness** (weekly cohorts; daily; refreshes every 5 min).
235
+
236
+ The `report-description-weak` validator check WARNs on short descriptions.
237
+ Descriptive column names in DataSources matter for the same reason — the
238
+ assistant reads them from data.json to decide which dataset answers a
239
+ question.
240
+
241
+ <!-- topic: components -->
242
+ ## Component Reuse Policy
243
+
244
+ **Before writing any custom HTML, CSS, or JS, name the framework component you ruled out and why.** Most needs are already solved.
245
+
246
+ | Need | Required component |
247
+ |---|---|
248
+ | Filters / dropdowns / sticky filter bar | `DataSource` + `FilterBar` |
249
+ | Section-local filter on same data (no propagate_to wiring) | `ScopedDataSource(id, parent=...)` + section-scoped `FilterBar` |
250
+ | Cascading dropdowns (parent → child option narrowing) | Add `depends_on: "parent_col"` to a dropdown filter spec |
251
+ | KPI cards | `KpiRow(dataset_id=...)` (`agg`: `sum` / `ratio` / `count` / `abssum`) |
252
+ | Time series | `LineChart` |
253
+ | Bar / stacked bar | `BarChart` / `StackedBar` |
254
+ | Area / stacked area | `AreaChart` (`stacked=True`) |
255
+ | Doughnut / pie | `DoughnutChart` |
256
+ | Heatmap / day×hour / cohort grid | `HeatmapChart` |
257
+ | Funnel | `FunnelChart` |
258
+ | Treemap | `TreemapChart` |
259
+ | Scatter / correlation / bubble | `ScatterChart` |
260
+ | Dual-axis bar + line | `ComboChart` |
261
+ | Ratio metrics (ARPDAU, ARPPU, retention, conversion) | `LineChart(ratios=[{numerator, denominator, label}])` — never hand-roll the division in JS |
262
+ | Ratio by category (CPD per comfort_zone, ARPPU by country) | `BarChart(ratios=[...], horizontal=True, sort='desc')` — categorical ratio bars |
263
+ | Tables | `DataTable`, `ComparisonTable`, `PivotTable` |
264
+ | Layout | `Grid`, `Panel`, `SplitPane`, `TabGroup` |
265
+ | Card-panel grid | `Grid(card=True)` |
266
+ | Cross-grain filter sync | `FilterBar(propagate_to={target_ds: {src_col: tgt_col}})` |
267
+ | A/B test report | `ABCompare.from_users(df, variant_col=..., control=..., test=..., metrics=[Metric(...)])` — the front door: a per-user frame in YOUR column names, out comes the finished component with an SRM badge and raw / winsor / CUPED modes (a `Metric` that declares `pre_col` opts into CUPED). Groups fetched separately (one query per arm)? `ABCompare.from_groups({"Control": df_a, "Test": df_b}, metrics=[...])`. Self-contained — no DataSource/FilterBar needed. The raw constructor `ABCompare(rows=..., modes=...)` plus `trellum.stats.ab.{winsorize_user_df, cuped_user_df, bootstrap_ab_cis}` remains for custom row sets. See `trellum/README.md` "A/B Testing". |
268
+
269
+ `RawHTML` is the **exception, not the default**. It is permitted only when **both**:
270
+
271
+ 1. The visualization is genuinely novel (force-directed graph, Sankey, etc.) and no combination of framework components can express it.
272
+ 2. You have explicitly named the components you considered and why each was insufficient.
273
+
274
+ For complex reports prefer the **hybrid pattern**: framework components for filters/standard charts/KPIs; `RawHTML` only for the genuinely novel section. The custom JS must subscribe to the framework filter engine — `window._fwFilterEngine.subscribe(dsId, id, fn)` — instead of managing its own filter state. See `trellum/README.md` "Custom Dashboards" for the full pattern and the `fw.*` API.
275
+
276
+ ### Anti-patterns to avoid
277
+
278
+ - **Hardcoded hex colors anywhere in RawHTML JS/HTML** — the `rawhtml-hardcoded-hex` validator **FAILS** the report on any `#RGB`/`#RRGGBB` literal except `#fff` and `#000`. Use `fw.getThemeColors().chart_colors[i]` for chart palettes and CSS vars (`var(--accent-red)`, `var(--text-main)`, `var(--bg-card)`) for HTML/CSS.
279
+ - **Hardcoded `rgba?()`/`rgb()` literals in RawHTML JS** — `rawhtml-hardcoded-rgba` (WARN) flags numeric color literals that don't update on theme switch. Use `fw.getThemeColors().grid_color` for grid lines, `.tick_color` for axis ticks / legend labels, `.chart_colors[i]` for dataset colors. `rgba(0,0,0,0)` (transparent) is exempt. Suppress for intentional fixed-color semantic annotations.
280
+ - **CSS variable strings in Chart.js color properties** — `rawhtml-css-var-in-chartjs` (WARN) flags patterns like `color: 'var(--text-main)'`. Chart.js has no CSS resolver; the string is used as-is (invalid color). Use `fw.getThemeColors().tick_color` / `.grid_color`, or resolve with `getComputedStyle(document.documentElement).getPropertyValue('--name').trim()`.
281
+ - Re-implementing Slim Select dropdowns (use `FilterBar`; `ScopedDataSource` for section-local; `depends_on` for cascading)
282
+ - Writing custom `getFilteredRows()` / filter state management (use the public `fw.filterEngine.*` API)
283
+ - Polling with `setTimeout(init, 100)` to wait for `_fwFilterEngine` (use `fw.filterEngine.onReady(dsId, fn)`)
284
+ - Referencing `window._fwFilterEngine` directly in RawHTML (it's private; use `fw.filterEngine`)
285
+ - Duplicating a DataFrame to get a separate DataSource for section-local filtering (use `ScopedDataSource(id, parent=...)`)
286
+ - Duplicating KPI rendering when `KpiRow(dataset_id=...)` covers it
287
+ - Building custom layout grids when `Grid` / `Panel` / `SplitPane` suffice
288
+ - Going 100% `RawHTML` when only one or two sections need custom logic
289
+ - LEFT JOINing coarser-grain data (monthly MAU, install cohort) into a finer-grain DataFrame to avoid creating a second DataSource — duplicates rows and silently breaks ratio aggregations under filtering
290
+
291
+ ### KpiRow aggregation semantics (exact, from the runtime)
292
+
293
+ Every `agg` recomputes over the **filtered rows**, so pass raw columns and let the runtime do the arithmetic — a pre-divided or pre-summed column cannot re-aggregate. The complete set:
294
+
295
+ | `agg` | Computes | Notes |
296
+ |---|---|---|
297
+ | `sum` | `sum(column)` — or summed across a `columns` list | the default |
298
+ | `count` | number of filtered rows | no `column` needed; add a literal `df["x_n"] = 1` column when you also need a ratio denominator |
299
+ | `abssum` | `sum(abs(column))` | for signed ledgers |
300
+ | `ratio` | `sum(numerator) / abs(sum(denominator)) × 100` | **always ×100 — only correct with `format: "percent"`.** A currency or plain-number ratio (AOV, ARPDAU, revenue per order) does NOT belong in a KpiRow: put it on a chart via `ratios=[{numerator, denominator, label}]`, which divides without the ×100 and formats per axis |
301
+ | `avg_by_date` | `sum(column) / count(distinct date_col)` | per-day average; `date_col` defaults to `event_date` |
302
+ | `purchase_pct` | share of `source_col` volume where `type_col` is in `match_values`, ×100 | percent-format only, like `ratio` |
303
+
304
+ The ×100 in `ratio` is the most re-derived fact in measured sessions — one agent spent 15 tool calls reading `kpis.py`, `validation.py` and the JS runtime to establish it. It is stated here so the next one does not have to.
305
+
306
+ <!-- topic: rawhtml -->
307
+ ## RawHTML chart lifecycle (mandatory when the chart lives inside a `Visible`)
308
+
309
+ If your `RawHTML` creates a Chart.js instance and the `RawHTML` is anywhere inside a toggle-driven `Visible`, the JS **must** patch BOTH `_initToggleVis` and `renderAll`, AND defer the render with `requestAnimationFrame`:
310
+
311
+ ```js
312
+ function _refresh() {
313
+ if (fw.filterEngine.isReady(dsId)) _render(canvasId, dsId, fw.filterEngine.getFiltered(dsId));
314
+ }
315
+
316
+ fw.filterEngine.subscribe(dsId, 'mychart', _render);
317
+ window.addEventListener('fw-theme-change', _refresh);
318
+
319
+ // _initToggleVis runs on initial page load (renderAll does NOT — the framework
320
+ // calls _renderComps() directly on first load). Without this hook the chart is
321
+ // blank on first load and on URL-preloaded state.
322
+ window._initToggleVis = (function (prev) {
323
+ return function () { if (prev) prev(); requestAnimationFrame(_refresh); };
324
+ })(window._initToggleVis);
325
+
326
+ // renderAll catches scope switches, theme changes, auto-refresh.
327
+ window.renderAll = (function (prev) {
328
+ return function () { if (prev) prev(); requestAnimationFrame(_refresh); };
329
+ })(window.renderAll);
330
+ ```
331
+
332
+ Why each piece:
333
+
334
+ | Hook | Catches | Without it |
335
+ |---|---|---|
336
+ | `fw.filterEngine.subscribe(dsId, ...)` | filter changes | chart never updates when user filters |
337
+ | `fw-theme-change` listener | theme switches | chart keeps old palette |
338
+ | `window._initToggleVis` patch | **initial page load + URL preload** | chart blank on first load — only renders after user clicks a toggle |
339
+ | `window.renderAll` patch | scope switches, auto-refresh, theme switches | chart stale after re-renders |
340
+ | `requestAnimationFrame` defer | layout reflow after `_updateToggleVis` flips `display` | Chart.js measures canvas at 0×0 → blank chart even though parent is visible |
341
+
342
+ For multi-canvas patterns (one canvas per per-feature DataSource), use a `_ensureSubs()` that finds canvases by attribute (e.g. `[data-split-ds]`) and lazily subscribes once each, then `requestAnimationFrame(_refreshAll)` from both lifecycle hooks.
343
+
344
+ <!-- topic: filters -->
345
+ ## DataSource + FilterBar rules
346
+
347
+ - Every report has at least one `DataSource` + `FilterBar` pair (exceptions: `ABCompare`-only reports; single-day dashboards may omit the date_range filter but should still use `DataSource` for any filterable dimension).
348
+ - `date_range` filter first when there's a time-series dimension; one filter per useful categorical dimension.
349
+ - **One grain = one DataSource.** Different grain (monthly MAU vs daily revenue) gets its own `DataSource`. Use `propagate_to` to sync shared dimensions.
350
+ - `static=True` only when data is fundamentally incompatible with the report's filters (point-in-time snapshot, external system data with different date semantics) — rare.
351
+
352
+ ### Two layout patterns: main vs section FilterBar
353
+
354
+ The framework supports two FilterBar placements, and they compose:
355
+
356
+ **Main FilterBar** (the default for most reports). Placed in an
357
+ **untitled section** at the top. Use it for filters that apply across
358
+ the whole report — typically `date_range`, `audience_segment`,
359
+ `platform`, etc. The main FilterBar reaches its primary DataSource
360
+ plus any DataSources listed in `propagate_to`. Sticks to the viewport
361
+ top.
362
+
363
+ ```python
364
+ ctx.add_section("", [ # untitled section — sticky top
365
+ DataSource("daily", df_daily, chunk_by="month"),
366
+ DataSource("monthly", df_monthly),
367
+ FilterBar("daily", df_daily, filters=[
368
+ {"column": "event_date", "type": "date_range"},
369
+ {"column": "platform"},
370
+ ], propagate_to={
371
+ "monthly": {"event_date": "event_date"},
372
+ }),
373
+ ])
374
+ ```
375
+
376
+ **Section-scoped FilterBar** (drill-downs). Placed inside a titled
377
+ section right after that section's DataSource(s). Use it when a
378
+ section has a dimension that doesn't make sense for the rest of the
379
+ report — e.g. `chest_level` only applies to chest charts. Section
380
+ FilterBars compose with the main FilterBar: a chart inside a section
381
+ is filtered by the AND of both. Sticks below the main FilterBar while
382
+ its section is in view.
383
+
384
+ Canonical layout for a section-scoped pair (validator recognizes this
385
+ exact order):
386
+
387
+ ```python
388
+ ctx.add_section("Chest Daily Trends", [
389
+ DataSource("chest_daily", df_chest),
390
+ FilterBar("chest_daily", df_chest, filters=[
391
+ {"column": "chest_level"},
392
+ {"column": "difficulty_tier"},
393
+ ]),
394
+ LineChart(..., dataset_id="chest_daily"),
395
+ LineChart(..., dataset_id="chest_daily"),
396
+ ])
397
+ ```
398
+
399
+ ### Section-local filters on the SAME data — use `ScopedDataSource`
400
+
401
+ When a section needs **another local filter** on data that's already
402
+ loaded via the main DataSource (e.g. "Price Tier" filter that only
403
+ affects one chart, not the whole report), use a `ScopedDataSource`
404
+ instead of a fresh DataSource. It:
405
+
406
+ - inherits all of the parent's filters automatically — no `propagate_to`
407
+ wiring needed
408
+ - ships no extra data — rows are derived from the parent at runtime
409
+ - composes with the section's own FilterBar (parent filters AND child
410
+ filters)
411
+
412
+ ```python
413
+ # Untitled top section: ONE base DataSource + main FilterBar
414
+ ctx.add_section("", [
415
+ DataSource("cpd", df),
416
+ FilterBar("cpd", df, filters=[
417
+ {"column": "event_date", "type": "date_range"},
418
+ {"column": "package_group"},
419
+ ]),
420
+ ])
421
+
422
+ # Drill-down section with its OWN local filter
423
+ ctx.add_section("CPD by Price Point", [
424
+ ScopedDataSource("cpd_pp", parent="cpd"),
425
+ FilterBar("cpd_pp", df, filters=[
426
+ {"column": "price_tier"}, # only affects this section
427
+ ]),
428
+ LineChart(df=df, x="event_date", dataset_id="cpd_pp",
429
+ ratios=[{"numerator": "chips", "denominator": "revenue",
430
+ "label": "CPD"}],
431
+ stack_by="price_point_display",
432
+ title="CPD by Price Point"),
433
+ ])
434
+ ```
435
+
436
+ **Cascading dropdowns** (e.g. *Package Group* → *Package Name*): add
437
+ `depends_on` to the child filter spec. The child's option list narrows
438
+ automatically when the parent selection changes:
439
+
440
+ ```python
441
+ FilterBar("cpd_pkg", df, filters=[
442
+ {"column": "package_group"},
443
+ {"column": "package_name", "depends_on": "package_group"},
444
+ ])
445
+ ```
446
+
447
+ ### Custom RawHTML lifecycle
448
+
449
+ When you do need custom JS (genuinely novel visualization), always use
450
+ the public `fw.*` API:
451
+
452
+ ```js
453
+ fw.filterEngine.onReady('cpd', function() {
454
+ // engine is ready — wire your chart now
455
+ fw.filterEngine.subscribe('cpd', 'my-chart', _render);
456
+ _render(fw.filterEngine.getFiltered('cpd'));
457
+ });
458
+ ```
459
+
460
+ **Never** poll with `setTimeout(init, 100)`, **never** reference
461
+ `window._fwFilterEngine` directly, **never** instantiate `new SlimSelect(...)`
462
+ in RawHTML. Each of those is a validator WARN now. Use FilterBar +
463
+ ScopedDataSource for the dropdown UX; use `fw.filterEngine.onReady` for
464
+ ready-detection; use `fw.filterEngine.*` for everything filter-related.
465
+
466
+ Validator constraints:
467
+
468
+ - At most one main FilterBar per report (untitled top section).
469
+ - At most one section-scoped FilterBar per titled section.
470
+ - Section-scoped pair must be laid out as `[DataSource(s) or ScopedDataSource, FilterBar, ...content]` at the start of the section.
471
+ - A `ScopedDataSource`'s `parent` must reference a base `DataSource` (not another scoped child).
472
+ - A filter with `depends_on` must reference another filter on the same FilterBar with a real column in the underlying DataFrame.
473
+
474
+ Use `ScopedDataSource` when a section filters the *same* rows more narrowly. When the grain genuinely differs, give each section its own `DataSource` instead — different data is not a scope of the same data.
475
+
476
+ <!-- topic: review -->
477
+ ## The live review loop
478
+
479
+ Review mode turns a served report into a feedback surface: the user clicks
480
+ elements in the browser, types change requests, queues them with one optional
481
+ chat message, and hits **Send** — you receive the batch in the terminal with
482
+ each item's section title, component kind and title, DOM id, and selector,
483
+ which map straight to lines of `generator.py`.
484
+
485
+ The loop, from your seat:
486
+
487
+ 1. `python -m trellum review start reports/<slug>` — builds if output is
488
+ missing (`--rebuild` to force), starts or reuses the background server,
489
+ enables review mode, and opens the browser. If a server predating review
490
+ mode holds the port, rerun with `--restart-server`.
491
+ 2. `python -m trellum review poll` — **BLOCKS** until the user sends
492
+ feedback. From an agent harness, run it with a generous timeout or as a
493
+ tracked background task; if it gets killed, just rerun it — queued
494
+ feedback is never lost. Exit codes: `0` feedback arrived, `2` your
495
+ `--timeout` elapsed, `3` the user ended the session.
496
+ 3. Edit `reports/<slug>/generator.py` and rebuild with
497
+ `python -m trellum.run reports/<slug> --no-serve`. The browser notices
498
+ the rebuild and reloads itself within ~2 seconds — never hand-edit
499
+ `output/` HTML, and never restart the server to "refresh".
500
+ 4. `python -m trellum review poll --reply "what you changed"` — the reply
501
+ appears in the browser's chat and unlocks the user's Send button, then
502
+ the command waits for the next round.
503
+ 5. Repeat until poll exits `3` (the user pressed End, or Send & End — that
504
+ final batch still arrives first). `python -m trellum review end` ends
505
+ it from your side when the user asks you to wrap up in conversation.
506
+
507
+ **Several reports, several agents, one project:** one server serves the
508
+ whole `output/` tree and one review session spans it — every report page
509
+ gets the overlay, and batches are tagged with their report's slug. When more
510
+ than one agent works the same project, each MUST poll with
511
+ `--report <slug>` (and reply with `--report <slug>`): a slug-scoped poll
512
+ drains only that report's batches, so agents never steal each other's
513
+ feedback. A slug-less poll drains everything — fine only when you are the
514
+ only agent. Different projects are automatically separate: each output
515
+ directory gets its own server on its own port (discovered via
516
+ `/_fw/server.json` identity on ports 8050–8069), with fully independent
517
+ review state.
518
+
519
+ The user can also send element-free chat messages; they arrive as a batch
520
+ whose `items` list is empty and whose message is the `note`. And on Chart.js
521
+ charts they can point at the data itself — Alt-click picks the nearest data
522
+ point, dragging marks an x-range — which arrives as a `data` field on the
523
+ item (`chart-point`: series/x/value; `chart-range`: x_from/x_to plus
524
+ per-series n/min/max and the points). That is a data investigation, not a
525
+ styling request: answer it with `trellum query` against the exact dates
526
+ before touching any code. Everything is served from the normal `trellum
527
+ serve` server — review endpoints live under `/_fw/review/`, loopback-only,
528
+ and the on-disk HTML is never modified (the overlay is injected into the
529
+ served copy only).
530
+
531
+ <!-- topic: validation -->
532
+ ## Validator response protocol (mandatory)
533
+
534
+ Every report run executes `trellum/validation/` post-generation. Output goes to console + `output/<slug>/_validation.json`.
535
+
536
+ A report task is **not done** until you have:
537
+
538
+ 1. Read the validator output.
539
+ 2. Surfaced every **FAIL** and **WARN** to the user with check ID, affected component/section, and a concrete fix.
540
+ 3. **Fixed every FAIL.** WARNs should be fixed unless intentionally accepted (then suppress in `report.yaml` under `validation.suppress` with a one-line reason for why).
541
+ 4. If the report has zero FAILs and zero WARNs, said so explicitly.
542
+ 5. **Handed the user a clickable link.** The last action before reporting done is `python -m trellum serve --background` — it starts (or reuses) a detached server, prints the URLs, and returns immediately — and your final message to the user must contain the printed report URL. A finished build the user cannot click is not finished. (Keep using `--no-serve` for the build/iterate loop itself; the link is the closing move, not a per-build cost.)
543
+
544
+ Severity:
545
+
546
+ | Level | Action |
547
+ |---|---|
548
+ | **FAIL** | Must fix. Structural bug → broken rendering, missing interactivity, wrong data. Common: missing `dataset_id`, orphan FilterBar, missing DataFrame columns. |
549
+ | **WARN** | Fix unless accepted + suppressed. Common: non-date x-axis with annotations enabled (silently skipped), hardcoded colors (breaks on theme switch), missing `renderAll` in custom JS. |
550
+ | **INFO** | No action required. Just confirm intent. |
551
+
552
+ Full check list, suppression syntax, and how to add new checks: `trellum/README.md` "Validation".
553
+
554
+ ### Filter coverage check
555
+
556
+ The validator emits **`chart-filter-coverage`** (WARN) for every chart whose `dataset_id` is not reached by one or more FilterBar filters — the most common silent bug, where users see filter dropdowns that change nothing on certain charts. The fix is almost always to extend `FilterBar.propagate_to` with the missing `{src_col: tgt_col}` entry.
557
+
558
+ A sibling check, **`chart-value-grain-mismatch`** (WARN), catches a subtler failure mode: the chart's value column is constant within each x-axis value (e.g. a daily total merged on `event_date` only into a date × audience × spender DataSource). Filters reach the DataSource structurally, but the chart's plotted value never changes — ratios stay constant, absolutes inflate by the count of replicated rows. Fix by extracting the column to its own DataSource at native grain (date-only) and filtering by `event_date` only there.
559
+
560
+ When a filter genuinely cannot apply to a DataSource (its SQL doesn't carry that dimension and adding it would require fictional attribution — e.g. attaching a `template_type` to a purchase event), accept the gap **per (DataSource, filter_column)** in `report.yaml`:
561
+
562
+ ```yaml
563
+ validation:
564
+ accept_inactive_filters:
565
+ tc_rev_gop3:
566
+ - template_type # purchases don't carry a template_type — see queries.py:156
567
+ tc_boost_gop3:
568
+ - template_type # boosters live in economy_balance, no template dim
569
+ - chest_tier
570
+ ```
571
+
572
+ This is the **preferred** form because future dead filters on the same DataSource still fire — only the explicitly-listed columns are silenced. The broader `validation.suppress_per_dataset: {ds: [chart-filter-coverage]}` swallows ALL future inactive filters on that DS and should be reserved for DataSources that are fundamentally non-reactive (tiny static rollups feeding one chart).
573
+
574
+ Full per-chart, per-filter matrix lives at `output/<slug>/_details.json` (also embedded in `_meta.json` under `details.filter_matrix`) and can be rendered as a coverage table. `details.totals` carries dataset count, chart count, total row count, total serialized data size — so you can see at a glance how heavy a report is. There is also a `data_source: "real" | "mock"` flag and an amber banner when the run was a mock/test build, so synthetic row counts don't get read as production reality.
575
+
576
+ With the health view enabled (`?health=1` in the URL, or an embedding application that reports the viewer as an admin), the rendered report itself shows a small **filter-health badge** above each chart whose dimensions are not all reactive — a red `⚠ N/M filters dead` chip that pops a styled per-filter detail panel on hover. Hidden by default for non-admins so end-users see a clean report.
577
+
578
+
579
+ ### Suppression hierarchy
580
+
581
+ Three layers, narrowest → broadest:
582
+
583
+ ```yaml
584
+ validation:
585
+ # Granular, PREFERRED — accept specific (DataSource, filter_column) pairs
586
+ # as inactive. Any OTHER inactive filter on the same DS still fires.
587
+ accept_inactive_filters:
588
+ pf_country_gop3: [platform]
589
+ pf_cz_gop3: [platform, spender_tier]
590
+
591
+ # Per-DataSource — silences the named check ID for ALL filters on that DS.
592
+ # Use only when the DataSource is fundamentally non-reactive across every
593
+ # dimension (tiny static rollup feeding one chart).
594
+ suppress_per_dataset:
595
+ economy_test_net: [ds-no-filterbar]
596
+
597
+ # Global — silences every instance of the named check ID across the report.
598
+ # Last resort; use for systemic exemptions (e.g. annotations-non-date-x-axis
599
+ # on a report that is intentionally month-bucketed).
600
+ suppress:
601
+ - some-systemic-check
602
+ ```
603
+
604
+ Suppressed checks are NOT removed from the result — they're flagged with `suppressed: true` and grouped under **Suppressed** with rationale text. The matrix renders suppressed inactive cells as `✗ⓢ` so a reviewer can audit later. Always pair a suppression with a `# rationale:` YAML comment naming the specific reason.
605
+
606
+ ## After framework changes (only if explicitly asked)
607
+
608
+ If the task explicitly requires modifying `trellum/*`:
609
+
610
+ 1. **Add or update validation checks in `trellum/validation/`** for the new behavior — non-negotiable. Skipping silently breaks future reports.
611
+ 2. Run unit tests:
612
+ - `python3 -m pytest trellum/testing/test_components.py -v` — component changes
613
+ - `python3 -m pytest trellum/testing/test_js_runtime.py -v` — JS runtime changes
614
+ - `python3 -m pytest trellum/testing/test_interactions.py -v` — theme / rendering changes
615
+ 3. Rebuild a couple of this project's existing reports to verify nothing broke —
616
+ pick ones that between them cover charts, KPIs, filters and any RawHTML:
617
+ ```bash
618
+ python3 -m trellum.run reports/<slug> --no-serve
619
+ ```
620
+ 4. For broader coverage, `python3 -m trellum.run --all --no-serve`.
621
+
622
+ ## This repo is a submodule — how changes reach reports
623
+
624
+ The framework is mounted as a git submodule at `trellum/` inside a consumer
625
+ repository — a report project, or an application that serves several of them.
626
+ Consumers pin a **release tag**, so a merged framework change does not reach
627
+ reports until a release is cut and the consumer bumps its submodule pointer.
628
+
629
+ Two consequences when working inside `trellum/`:
630
+
631
+ - **Commit on a branch, push to this repository.** Submodules check out
632
+ detached HEAD; commits made there belong to `trellum`, not the
633
+ consumer repo. `git checkout -b feature/x origin/develop` first.
634
+ - **Do not commit the moved submodule pointer on the consumer's main branch.**
635
+ That pointer is only bumped when moving to a released tag.
636
+
637
+ Releases are published as **GitHub Releases** (a tag alone is not a release).
638
+ There are no Actions runners for this repo, so it is a manual three-step
639
+ sequence — push the tag, `gh release create --generate-notes --latest`, then
640
+ fast-forward the `release` branch. `__version__` must match the tag.
641
+
642
+ Branch model, versioning rules, the release procedure, and the consumer bump
643
+ commands are all in [`RELEASING.md`](RELEASING.md). Read it before tagging a
644
+ release or changing `__version__`.
645
+
646
+ ## What NOT to do
647
+
648
+ - **Do not edit `trellum/*` while building a report.** Reports only touch `reports/{slug}/`. If a task seems to require framework changes, stop and discuss scope with the user — framework changes affect every report.
649
+ - **Do not run `--all` to verify a single change.** Build only the affected report (`python3 -m trellum.run reports/<slug> --no-serve`).
650
+ - **Do not start the preview server to check whether a build worked.** The build prints its own result and the validator writes `output/<slug>/_validation.json`. A bare `trellum.run` blocks until killed; `--no-serve` exits.
651
+ - **Do not write custom Chart.js code for any chart type listed above.** Use the framework component.
652
+ - **Do not hardcode colors.** CSS variables for HTML/CSS, `getThemeColors()` for Chart.js.
653
+ - **Do not import Chart.js or other JS libraries.** Bundled in `trellum/static/vendor/`.
654
+ - **Do not write connection / credential code.** Use `ctx.get_connection()`.
655
+ - **Do not use Chart.js native `legend: { display: true }` on non-doughnut charts.** The `fwLegend` plugin handles it.
656
+ - **Do not open `output/<slug>/index.html` as `file://`.** `data.json` is fetched via XHR and requires HTTP — the runner serves automatically on :8050.
657
+
658
+ ## Licensing (applies to every change)
659
+
660
+ This repository is MIT licensed.
661
+
662
+ - **Do not add per-file licence headers.** There are none, deliberately;
663
+ `LICENSE` governs the tree. A half-headered repository is worse than either
664
+ consistent choice.
665
+ - **Adding a dependency means updating `THIRD-PARTY.md` in the same commit.**
666
+ `docs/LICENSING.md` has the audit command. Anything permissive is fine;
667
+ anything copyleft or source-available needs a conversation first, because it
668
+ changes what a downstream user may do with a build of this repository.
669
+ - **Bundling is the stricter case than depending.** Code added under
670
+ `static/vendor/` ships inside every copy of this repository, so its notice
671
+ travels with it — record it in `static/vendor/MANIFEST.json`.