dataface 0.1.2__py3-none-any.whl

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 (455) hide show
  1. d3_format/__init__.py +14 -0
  2. d3_format/errors.py +19 -0
  3. d3_format/format.py +551 -0
  4. d3_format/spec.py +159 -0
  5. dataface/DATAFACE_SYNTAX.md +1135 -0
  6. dataface/__init__.py +93 -0
  7. dataface/_docs_site.py +20 -0
  8. dataface/_install_hint.py +26 -0
  9. dataface/agent_api/__init__.py +79 -0
  10. dataface/agent_api/_init_templates/__init__.py +0 -0
  11. dataface/agent_api/_init_templates/agents_dft_snippet.md +26 -0
  12. dataface/agent_api/_init_templates/dataface.yml +15 -0
  13. dataface/agent_api/_init_templates/faces-dataface.yml +144 -0
  14. dataface/agent_api/_init_templates/index.md +24 -0
  15. dataface/agent_api/_paths.py +118 -0
  16. dataface/agent_api/_project_agents_md.py +43 -0
  17. dataface/agent_api/_session_store.py +486 -0
  18. dataface/agent_api/_state.py +28 -0
  19. dataface/agent_api/chat.py +221 -0
  20. dataface/agent_api/dashboards.py +257 -0
  21. dataface/agent_api/describe.py +366 -0
  22. dataface/agent_api/describe_query.py +120 -0
  23. dataface/agent_api/docs/__init__.py +25 -0
  24. dataface/agent_api/docs/_loader.py +292 -0
  25. dataface/agent_api/docs/yaml-reference.md +2757 -0
  26. dataface/agent_api/file_refs.py +118 -0
  27. dataface/agent_api/init.py +126 -0
  28. dataface/agent_api/inspect.py +128 -0
  29. dataface/agent_api/mcp_install.py +170 -0
  30. dataface/agent_api/query.py +274 -0
  31. dataface/agent_api/schema.py +658 -0
  32. dataface/agent_api/schema_search.py +284 -0
  33. dataface/agent_api/search.py +270 -0
  34. dataface/agent_api/skill_install.py +141 -0
  35. dataface/agent_api/skill_render.py +90 -0
  36. dataface/agent_api/skills.py +293 -0
  37. dataface/agent_api/surface_aliases.yaml +128 -0
  38. dataface/agent_api/validate.py +175 -0
  39. dataface/agent_api/validate_query.py +84 -0
  40. dataface/ai/__init__.py +39 -0
  41. dataface/ai/agent.py +139 -0
  42. dataface/ai/context.py +45 -0
  43. dataface/ai/events.py +62 -0
  44. dataface/ai/external_mcp.py +610 -0
  45. dataface/ai/generate_sql.py +96 -0
  46. dataface/ai/llm.py +403 -0
  47. dataface/ai/mcp/__init__.py +51 -0
  48. dataface/ai/mcp/server.py +289 -0
  49. dataface/ai/memories.py +85 -0
  50. dataface/ai/prompts.py +177 -0
  51. dataface/ai/schema_context.py +138 -0
  52. dataface/ai/skills/before-after-comparison/SKILL.md +102 -0
  53. dataface/ai/skills/before-after-comparison/examples/before-after-comparison.yml +24 -0
  54. dataface/ai/skills/dashboard-build/SKILL.md +212 -0
  55. dataface/ai/skills/dashboard-build/examples/_smoke.yml +15 -0
  56. dataface/ai/skills/dashboard-design/SKILL.md +182 -0
  57. dataface/ai/skills/dashboard-review/SKILL.md +113 -0
  58. dataface/ai/skills/dashboard-structural-review/SKILL.md +173 -0
  59. dataface/ai/skills/dashboard-visual-review/SKILL.md +139 -0
  60. dataface/ai/skills/dataface-mcp-setup/SKILL.md +177 -0
  61. dataface/ai/skills/dataface-troubleshooting/SKILL.md +225 -0
  62. dataface/ai/skills/drill-down-link/SKILL.md +112 -0
  63. dataface/ai/skills/drill-down-link/examples/drill-down-link.yml +27 -0
  64. dataface/ai/skills/faceted-small-multiples/SKILL.md +116 -0
  65. dataface/ai/skills/faceted-small-multiples/examples/faceted-small-multiples.yml +33 -0
  66. dataface/ai/skills/filter-bar-with-variables/SKILL.md +105 -0
  67. dataface/ai/skills/filter-bar-with-variables/examples/filter-bar-with-variables.yml +49 -0
  68. dataface/ai/skills/kpi-row/SKILL.md +101 -0
  69. dataface/ai/skills/kpi-row/examples/kpi-row.yml +55 -0
  70. dataface/ai/skills/report-design/SKILL.md +184 -0
  71. dataface/ai/skills/single-metric-bignum/SKILL.md +90 -0
  72. dataface/ai/skills/single-metric-bignum/examples/single-metric-bignum.yml +27 -0
  73. dataface/ai/skills/table-heavy-ops-dashboard/SKILL.md +114 -0
  74. dataface/ai/skills/table-heavy-ops-dashboard/examples/table-heavy-ops-dashboard.yml +48 -0
  75. dataface/ai/skills/time-series-trend/SKILL.md +93 -0
  76. dataface/ai/skills/time-series-trend/examples/time-series-trend.yml +26 -0
  77. dataface/ai/skills/top-n-with-detail/SKILL.md +98 -0
  78. dataface/ai/skills/top-n-with-detail/examples/top-n-with-detail.yml +45 -0
  79. dataface/ai/skills/two-by-two-grid-overview/SKILL.md +78 -0
  80. dataface/ai/skills/two-by-two-grid-overview/examples/two-by-two-grid-overview.yml +64 -0
  81. dataface/ai/tool_schemas.py +132 -0
  82. dataface/ai/tools/__init__.py +312 -0
  83. dataface/ai/yaml_utils.py +57 -0
  84. dataface/cli/__init__.py +3 -0
  85. dataface/cli/_console.py +48 -0
  86. dataface/cli/_error_format.py +83 -0
  87. dataface/cli/_extras.py +190 -0
  88. dataface/cli/_json_output.py +8 -0
  89. dataface/cli/_parsing.py +17 -0
  90. dataface/cli/_version_info.py +56 -0
  91. dataface/cli/commands/__init__.py +3 -0
  92. dataface/cli/commands/_agent_input.py +205 -0
  93. dataface/cli/commands/_agent_server.py +115 -0
  94. dataface/cli/commands/chat.py +645 -0
  95. dataface/cli/commands/describe.py +107 -0
  96. dataface/cli/commands/docs.py +131 -0
  97. dataface/cli/commands/extension.py +179 -0
  98. dataface/cli/commands/init.py +240 -0
  99. dataface/cli/commands/inspect.py +94 -0
  100. dataface/cli/commands/mcp_init.py +167 -0
  101. dataface/cli/commands/query.py +386 -0
  102. dataface/cli/commands/render.py +291 -0
  103. dataface/cli/commands/schema.py +411 -0
  104. dataface/cli/commands/search.py +49 -0
  105. dataface/cli/commands/serve.py +114 -0
  106. dataface/cli/commands/skills.py +133 -0
  107. dataface/cli/commands/skills_init.py +161 -0
  108. dataface/cli/commands/validate.py +63 -0
  109. dataface/cli/main.py +1501 -0
  110. dataface/core/__init__.py +75 -0
  111. dataface/core/compile/__init__.py +244 -0
  112. dataface/core/compile/_jinja_helpers.py +78 -0
  113. dataface/core/compile/channel.py +222 -0
  114. dataface/core/compile/chart_focus.py +101 -0
  115. dataface/core/compile/chart_resolved.py +169 -0
  116. dataface/core/compile/chart_type_detection.py +489 -0
  117. dataface/core/compile/chart_update.py +261 -0
  118. dataface/core/compile/colors.py +64 -0
  119. dataface/core/compile/compiler.py +904 -0
  120. dataface/core/compile/config.py +823 -0
  121. dataface/core/compile/custom_chart_types.py +208 -0
  122. dataface/core/compile/data_table_attachment.py +1287 -0
  123. dataface/core/compile/detect.py +110 -0
  124. dataface/core/compile/errors.py +302 -0
  125. dataface/core/compile/filter_injection.py +319 -0
  126. dataface/core/compile/introspection.py +527 -0
  127. dataface/core/compile/jinja.py +511 -0
  128. dataface/core/compile/labels_env.py +52 -0
  129. dataface/core/compile/markdown.py +154 -0
  130. dataface/core/compile/meta.py +388 -0
  131. dataface/core/compile/models/__init__.py +0 -0
  132. dataface/core/compile/models/chart/__init__.py +0 -0
  133. dataface/core/compile/models/chart/authored.py +2137 -0
  134. dataface/core/compile/models/chart/compiled.py +398 -0
  135. dataface/core/compile/models/config.py +347 -0
  136. dataface/core/compile/models/face/__init__.py +0 -0
  137. dataface/core/compile/models/face/authored.py +659 -0
  138. dataface/core/compile/models/face/compiled.py +522 -0
  139. dataface/core/compile/models/factories.py +201 -0
  140. dataface/core/compile/models/markers.py +40 -0
  141. dataface/core/compile/models/palette.py +36 -0
  142. dataface/core/compile/models/primitives.py +415 -0
  143. dataface/core/compile/models/query/__init__.py +0 -0
  144. dataface/core/compile/models/query/authored.py +246 -0
  145. dataface/core/compile/models/query/compiled.py +710 -0
  146. dataface/core/compile/models/refs.py +137 -0
  147. dataface/core/compile/models/source.py +611 -0
  148. dataface/core/compile/models/style/__init__.py +0 -0
  149. dataface/core/compile/models/style/authored.py +481 -0
  150. dataface/core/compile/models/style/compiled.py +3399 -0
  151. dataface/core/compile/models/style/merged.py +1682 -0
  152. dataface/core/compile/models/theme.py +362 -0
  153. dataface/core/compile/models/variable/__init__.py +0 -0
  154. dataface/core/compile/models/variable/authored.py +254 -0
  155. dataface/core/compile/models/vega_lite/__init__.py +0 -0
  156. dataface/core/compile/models/vega_lite/config.py +510 -0
  157. dataface/core/compile/models/vega_lite/contracts.py +171 -0
  158. dataface/core/compile/normalize_charts.py +494 -0
  159. dataface/core/compile/normalize_layout.py +1000 -0
  160. dataface/core/compile/normalize_queries.py +297 -0
  161. dataface/core/compile/normalize_variables.py +489 -0
  162. dataface/core/compile/normalizer.py +543 -0
  163. dataface/core/compile/palette.py +1100 -0
  164. dataface/core/compile/parameterized.py +658 -0
  165. dataface/core/compile/parser.py +228 -0
  166. dataface/core/compile/schema.py +20 -0
  167. dataface/core/compile/schema_renderers/__init__.py +0 -0
  168. dataface/core/compile/schema_renderers/json_schema.py +163 -0
  169. dataface/core/compile/schema_renderers/prompt.py +152 -0
  170. dataface/core/compile/schema_renderers/vscode_schema.py +301 -0
  171. dataface/core/compile/sizing.py +2126 -0
  172. dataface/core/compile/sources.py +518 -0
  173. dataface/core/compile/sql_authoring_lint.py +56 -0
  174. dataface/core/compile/style_cascade.py +471 -0
  175. dataface/core/compile/typography.py +299 -0
  176. dataface/core/compile/validator.py +301 -0
  177. dataface/core/compile/variables.py +53 -0
  178. dataface/core/compile/vega_config.py +98 -0
  179. dataface/core/compile/vega_lite/__init__.py +6 -0
  180. dataface/core/compile/vega_lite/validation.py +95 -0
  181. dataface/core/compile/yaml_error_formatter.py +838 -0
  182. dataface/core/connections.py +38 -0
  183. dataface/core/dashboard.py +358 -0
  184. dataface/core/defaults/default_config.yml +101 -0
  185. dataface/core/defaults/palettes/categorical/category-10-dark.yml +32 -0
  186. dataface/core/defaults/palettes/categorical/category-10-light.yml +43 -0
  187. dataface/core/defaults/palettes/categorical/category-10.yml +31 -0
  188. dataface/core/defaults/palettes/categorical/category-6-tonal-blue.yml +22 -0
  189. dataface/core/defaults/palettes/categorical/category-6-tonal-brown.yml +29 -0
  190. dataface/core/defaults/palettes/categorical/category-6-tonal-green.yml +20 -0
  191. dataface/core/defaults/palettes/categorical/category-6-tonal-orange.yml +21 -0
  192. dataface/core/defaults/palettes/categorical/category-6-tonal-purple.yml +20 -0
  193. dataface/core/defaults/palettes/categorical/editorial-10-dark.yml +32 -0
  194. dataface/core/defaults/palettes/categorical/editorial-10.yml +40 -0
  195. dataface/core/defaults/palettes/categorical/hero-6.yml +17 -0
  196. dataface/core/defaults/palettes/categorical/single-blue.yml +11 -0
  197. dataface/core/defaults/palettes/categorical/tableau.yml +20 -0
  198. dataface/core/defaults/palettes/data/xkcd_colors.json +3803 -0
  199. dataface/core/defaults/palettes/diverging/blue-red.yml +25 -0
  200. dataface/core/defaults/palettes/diverging/coolwarm.yml +24 -0
  201. dataface/core/defaults/palettes/diverging/crimson-green.yml +23 -0
  202. dataface/core/defaults/palettes/diverging/orange-teal.yml +23 -0
  203. dataface/core/defaults/palettes/diverging/sunset.yml +24 -0
  204. dataface/core/defaults/palettes/scaffold/dft-creams.yml +38 -0
  205. dataface/core/defaults/palettes/scaffold/dft-grays.yml +53 -0
  206. dataface/core/defaults/palettes/sequential/amber.yml +22 -0
  207. dataface/core/defaults/palettes/sequential/blue.yml +22 -0
  208. dataface/core/defaults/palettes/sequential/brown.yml +22 -0
  209. dataface/core/defaults/palettes/sequential/gray.yml +22 -0
  210. dataface/core/defaults/palettes/sequential/green.yml +22 -0
  211. dataface/core/defaults/palettes/sequential/purple.yml +22 -0
  212. dataface/core/defaults/palettes/sequential/rust.yml +22 -0
  213. dataface/core/defaults/palettes/sequential/teal.yml +22 -0
  214. dataface/core/defaults/palettes/tone/negative.yml +32 -0
  215. dataface/core/defaults/palettes/tone/positive.yml +22 -0
  216. dataface/core/defaults/palettes/tone/warning.yml +22 -0
  217. dataface/core/defaults/themes/_base.yaml +786 -0
  218. dataface/core/defaults/themes/bi.yaml +16 -0
  219. dataface/core/defaults/themes/carbong100.yaml +41 -0
  220. dataface/core/defaults/themes/cream.yaml +122 -0
  221. dataface/core/defaults/themes/dark.yaml +40 -0
  222. dataface/core/defaults/themes/diagnostics-title-angle-extreme.yaml +9 -0
  223. dataface/core/defaults/themes/diagnostics-title-baseline-extreme.yaml +9 -0
  224. dataface/core/defaults/themes/diagnostics-title-baseline.yaml +24 -0
  225. dataface/core/defaults/themes/diagnostics-title-center.yaml +8 -0
  226. dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +24 -0
  227. dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +25 -0
  228. dataface/core/defaults/themes/diagnostics-title-left.yaml +8 -0
  229. dataface/core/defaults/themes/diagnostics-title-offset-extreme.yaml +9 -0
  230. dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +24 -0
  231. dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +24 -0
  232. dataface/core/defaults/themes/editorial.yaml +147 -0
  233. dataface/core/defaults/themes/light.yaml +30 -0
  234. dataface/core/defaults/themes/looker.yaml +17 -0
  235. dataface/core/defaults/themes/stark.yaml +134 -0
  236. dataface/core/errors/__init__.py +67 -0
  237. dataface/core/errors/codes_compile.py +56 -0
  238. dataface/core/errors/codes_execute.py +177 -0
  239. dataface/core/errors/codes_render.py +106 -0
  240. dataface/core/errors/codes_unknown.py +15 -0
  241. dataface/core/errors/hints.py +74 -0
  242. dataface/core/errors/registry.py +42 -0
  243. dataface/core/errors/structured.py +92 -0
  244. dataface/core/execute/__init__.py +91 -0
  245. dataface/core/execute/adapters/__init__.py +49 -0
  246. dataface/core/execute/adapters/adapter_registry.py +400 -0
  247. dataface/core/execute/adapters/base.py +245 -0
  248. dataface/core/execute/adapters/csv_adapter.py +239 -0
  249. dataface/core/execute/adapters/dbt_adapter.py +283 -0
  250. dataface/core/execute/adapters/dbt_adapter_factory.py +212 -0
  251. dataface/core/execute/adapters/dbt_macro_loader.py +95 -0
  252. dataface/core/execute/adapters/dbt_utils.py +150 -0
  253. dataface/core/execute/adapters/http_adapter.py +224 -0
  254. dataface/core/execute/adapters/metricflow_adapter.py +94 -0
  255. dataface/core/execute/adapters/schema_resolver_adapter.py +144 -0
  256. dataface/core/execute/adapters/sql_adapter.py +710 -0
  257. dataface/core/execute/adapters/values_adapter.py +58 -0
  258. dataface/core/execute/batch.py +744 -0
  259. dataface/core/execute/cache_backend.py +135 -0
  260. dataface/core/execute/cache_keys.py +66 -0
  261. dataface/core/execute/dbt_jinja.py +21 -0
  262. dataface/core/execute/dialects/__init__.py +121 -0
  263. dataface/core/execute/dialects/athena.py +75 -0
  264. dataface/core/execute/dialects/base.py +302 -0
  265. dataface/core/execute/dialects/bigquery.py +38 -0
  266. dataface/core/execute/dialects/databricks.py +68 -0
  267. dataface/core/execute/dialects/duckdb.py +35 -0
  268. dataface/core/execute/dialects/mysql.py +68 -0
  269. dataface/core/execute/dialects/postgres.py +39 -0
  270. dataface/core/execute/dialects/redshift.py +12 -0
  271. dataface/core/execute/dialects/snowflake.py +51 -0
  272. dataface/core/execute/dialects/sqlserver.py +92 -0
  273. dataface/core/execute/duckdb_cache.py +712 -0
  274. dataface/core/execute/duckdb_config.py +26 -0
  275. dataface/core/execute/errors.py +213 -0
  276. dataface/core/execute/executor.py +1249 -0
  277. dataface/core/execute/parallel.py +162 -0
  278. dataface/core/execute/setup_sql.py +58 -0
  279. dataface/core/execute/source_registry.py +72 -0
  280. dataface/core/execute/source_resolver.py +255 -0
  281. dataface/core/execute/sql_guard.py +387 -0
  282. dataface/core/execute/sql_literals.py +199 -0
  283. dataface/core/fonts.py +52 -0
  284. dataface/core/inspect/__init__.py +32 -0
  285. dataface/core/inspect/cache_factory.py +98 -0
  286. dataface/core/inspect/db_types.py +162 -0
  287. dataface/core/inspect/dbt_schema.py +96 -0
  288. dataface/core/inspect/defaults.yml +37 -0
  289. dataface/core/inspect/fanout_risk.py +109 -0
  290. dataface/core/inspect/manifest_utils.py +77 -0
  291. dataface/core/inspect/partials/categorical.yml +40 -0
  292. dataface/core/inspect/partials/date.yml +40 -0
  293. dataface/core/inspect/partials/numeric.yml +55 -0
  294. dataface/core/inspect/partition_types.py +38 -0
  295. dataface/core/inspect/query_validator.py +975 -0
  296. dataface/core/inspect/renderer.py +354 -0
  297. dataface/core/inspect/resolver.py +808 -0
  298. dataface/core/inspect/search.py +461 -0
  299. dataface/core/inspect/sources/__init__.py +32 -0
  300. dataface/core/inspect/sources/dbt.py +738 -0
  301. dataface/core/inspect/sources/duckdb_utils.py +66 -0
  302. dataface/core/inspect/templates/__init__.py +1 -0
  303. dataface/core/inspect/templates/categorical_column.yml +196 -0
  304. dataface/core/inspect/templates/charts.yml +109 -0
  305. dataface/core/inspect/templates/date_column.yml +248 -0
  306. dataface/core/inspect/templates/model.yml +138 -0
  307. dataface/core/inspect/templates/numeric_column.yml +261 -0
  308. dataface/core/inspect/templates/quality.yml +80 -0
  309. dataface/core/inspect/templates/string_column.yml +263 -0
  310. dataface/core/project_roots.py +165 -0
  311. dataface/core/render/__init__.py +87 -0
  312. dataface/core/render/board_links.py +176 -0
  313. dataface/core/render/chart/__init__.py +27 -0
  314. dataface/core/render/chart/arc_attached_table.py +251 -0
  315. dataface/core/render/chart/artifacts.py +16 -0
  316. dataface/core/render/chart/callout.py +225 -0
  317. dataface/core/render/chart/decisions.py +358 -0
  318. dataface/core/render/chart/geo.py +700 -0
  319. dataface/core/render/chart/kpi.py +916 -0
  320. dataface/core/render/chart/labels.py +76 -0
  321. dataface/core/render/chart/pipeline.py +818 -0
  322. dataface/core/render/chart/presentation.py +36 -0
  323. dataface/core/render/chart/profile.py +3438 -0
  324. dataface/core/render/chart/render_single.py +347 -0
  325. dataface/core/render/chart/renderers.py +193 -0
  326. dataface/core/render/chart/rendering.py +565 -0
  327. dataface/core/render/chart/serialization.py +90 -0
  328. dataface/core/render/chart/spark.py +496 -0
  329. dataface/core/render/chart/spark_bar.py +370 -0
  330. dataface/core/render/chart/spec_builders.py +154 -0
  331. dataface/core/render/chart/standard_renderer.py +2645 -0
  332. dataface/core/render/chart/table.py +2957 -0
  333. dataface/core/render/chart/table_support.py +1452 -0
  334. dataface/core/render/chart/tick_values.py +66 -0
  335. dataface/core/render/chart/time_unit_detect.py +809 -0
  336. dataface/core/render/chart/title_overflow.py +157 -0
  337. dataface/core/render/chart/type_inference.py +122 -0
  338. dataface/core/render/chart/validation.py +99 -0
  339. dataface/core/render/chart/vega_lite.py +125 -0
  340. dataface/core/render/chart/vega_lite_types.py +268 -0
  341. dataface/core/render/chart/vl_field_maps.py +346 -0
  342. dataface/core/render/chart_interactivity.py +24 -0
  343. dataface/core/render/control_registry.py +287 -0
  344. dataface/core/render/converters/__init__.py +24 -0
  345. dataface/core/render/converters/chart.py +276 -0
  346. dataface/core/render/converters/html.py +98 -0
  347. dataface/core/render/converters/pdf.py +40 -0
  348. dataface/core/render/converters/png.py +41 -0
  349. dataface/core/render/errors.py +144 -0
  350. dataface/core/render/face_api.py +160 -0
  351. dataface/core/render/faces.py +1194 -0
  352. dataface/core/render/font_measurement.py +48 -0
  353. dataface/core/render/font_support.py +197 -0
  354. dataface/core/render/fonts/DFTSansTabular-Regular.ttf +0 -0
  355. dataface/core/render/fonts/DFTSansTabular-Regular.woff2 +0 -0
  356. dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.ttf +0 -0
  357. dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.ttf +0 -0
  358. dataface/core/render/fonts/InterVariable.ttf +0 -0
  359. dataface/core/render/fonts/InterVariable.woff2 +0 -0
  360. dataface/core/render/fonts/NOTO_COLOR_EMOJI_LICENSE.txt +93 -0
  361. dataface/core/render/fonts/NOTO_EMOJI_LICENSE.txt +93 -0
  362. dataface/core/render/fonts/NotoColorEmoji-Regular.ttf +0 -0
  363. dataface/core/render/fonts/NotoColorEmoji-Regular.woff2 +0 -0
  364. dataface/core/render/fonts/NotoEmoji-Regular.ttf +0 -0
  365. dataface/core/render/fonts/NotoEmoji-Regular.woff2 +0 -0
  366. dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -0
  367. dataface/core/render/fonts/SOURCE_SERIF_4_LICENSE.txt +98 -0
  368. dataface/core/render/fonts/SourceCodePro-Regular.ttf +0 -0
  369. dataface/core/render/fonts/SourceSerif4-Regular.ttf +0 -0
  370. dataface/core/render/fonts/_emoji_font_face.css +43 -0
  371. dataface/core/render/fonts/source-serif-4-variable-latin.woff2 +0 -0
  372. dataface/core/render/format_utils.py +329 -0
  373. dataface/core/render/geo_defaults.yml +28 -0
  374. dataface/core/render/json_format.py +146 -0
  375. dataface/core/render/layout_sizing.py +865 -0
  376. dataface/core/render/layouts.py +541 -0
  377. dataface/core/render/markdown_defaults.yml +16 -0
  378. dataface/core/render/missing_vars_prompt.py +79 -0
  379. dataface/core/render/placeholder.py +389 -0
  380. dataface/core/render/render_result.py +14 -0
  381. dataface/core/render/renderer.py +467 -0
  382. dataface/core/render/script_embedding.py +16 -0
  383. dataface/core/render/svg_utils.py +212 -0
  384. dataface/core/render/template_loader.py +69 -0
  385. dataface/core/render/templates/controls/_styles.css +606 -0
  386. dataface/core/render/templates/controls/checkbox.html +16 -0
  387. dataface/core/render/templates/controls/date.html +16 -0
  388. dataface/core/render/templates/controls/number.html +19 -0
  389. dataface/core/render/templates/controls/readonly.html +9 -0
  390. dataface/core/render/templates/controls/select.html +21 -0
  391. dataface/core/render/templates/controls/slider.html +22 -0
  392. dataface/core/render/templates/controls/text.html +16 -0
  393. dataface/core/render/templates/scripts/chart_interactivity.js +191 -0
  394. dataface/core/render/templates/scripts/variables.js +976 -0
  395. dataface/core/render/templates/svg/grid_pattern.svg +3 -0
  396. dataface/core/render/templates/svg/styles.css +51 -0
  397. dataface/core/render/terminal.py +311 -0
  398. dataface/core/render/terminal_charts.py +563 -0
  399. dataface/core/render/terminal_defaults.yml +2 -0
  400. dataface/core/render/terminal_layouts.py +299 -0
  401. dataface/core/render/terminal_text.py +31 -0
  402. dataface/core/render/text/__init__.py +1 -0
  403. dataface/core/render/text/case.py +113 -0
  404. dataface/core/render/text_format.py +129 -0
  405. dataface/core/render/utils.py +106 -0
  406. dataface/core/render/variable_controls.py +946 -0
  407. dataface/core/render/variable_input_refinement.py +140 -0
  408. dataface/core/render/warnings/__init__.py +15 -0
  409. dataface/core/render/warnings/bar_color_1_to_1_with_x.py +80 -0
  410. dataface/core/render/warnings/base.py +44 -0
  411. dataface/core/render/warnings/fanout_risk.py +15 -0
  412. dataface/core/render/warnings/from_query_diagnostic.py +56 -0
  413. dataface/core/render/warnings/missing_join_predicate.py +13 -0
  414. dataface/core/render/warnings/query_parse_error.py +14 -0
  415. dataface/core/render/warnings/query_returned_zero_rows.py +42 -0
  416. dataface/core/render/warnings/reaggregation.py +14 -0
  417. dataface/core/render/warnings/registry.py +45 -0
  418. dataface/core/render/warnings/suppression.py +46 -0
  419. dataface/core/render/warnings/temporal_single_point.py +63 -0
  420. dataface/core/render/warnings/unreferenced_chart.py +15 -0
  421. dataface/core/render/warnings/y_encoding_mostly_null.py +76 -0
  422. dataface/core/render/yaml_format.py +167 -0
  423. dataface/core/resolve_face.py +195 -0
  424. dataface/core/schema/__init__.py +0 -0
  425. dataface/core/schema/guidance.py +151 -0
  426. dataface/core/scoped_paths.py +59 -0
  427. dataface/core/serve/__init__.py +14 -0
  428. dataface/core/serve/bootstrap.py +39 -0
  429. dataface/core/serve/embedded.py +57 -0
  430. dataface/core/serve/port.py +129 -0
  431. dataface/core/serve/server.py +938 -0
  432. dataface/core/serve/templates/__init__.py +0 -0
  433. dataface/core/serve/templates/directory.yml +6 -0
  434. dataface/core/serve/templates/error.html.j2 +217 -0
  435. dataface/core/utils.py +121 -0
  436. dataface/core/validate.py +64 -0
  437. dataface/integrations/__init__.py +0 -0
  438. dataface/integrations/highlighting.py +351 -0
  439. dataface/integrations/markdown.py +537 -0
  440. dataface/py.typed +0 -0
  441. dataface-0.1.2.dist-info/METADATA +375 -0
  442. dataface-0.1.2.dist-info/RECORD +455 -0
  443. dataface-0.1.2.dist-info/WHEEL +4 -0
  444. dataface-0.1.2.dist-info/entry_points.txt +2 -0
  445. dataface-0.1.2.dist-info/licenses/LICENSE +202 -0
  446. mdsvg/__init__.py +168 -0
  447. mdsvg/fonts.py +656 -0
  448. mdsvg/images.py +299 -0
  449. mdsvg/parser.py +629 -0
  450. mdsvg/playground.py +284 -0
  451. mdsvg/py.typed +2 -0
  452. mdsvg/renderer.py +1623 -0
  453. mdsvg/style.py +355 -0
  454. mdsvg/types.py +200 -0
  455. mdsvg/utils.py +86 -0
@@ -0,0 +1,90 @@
1
+ """Expand ``{{ s_<key> }}`` macros in wheel skill bodies.
2
+
3
+ Skill markdown is authored once with ``{{ s_<key> }}`` tokens. On the MCP
4
+ surface the ``mcp`` side of the alias renders; on the CLI surface the ``dft``
5
+ side renders. The alias table lives in ``surface_aliases.yaml`` next to this
6
+ module so every wheel skill and every surface points at the same source of
7
+ truth.
8
+
9
+ Skills that should never appear on one surface (e.g. ``dataface-mcp-setup`` is
10
+ CLI-only) declare ``surfaces:`` in their frontmatter; the registry hides them
11
+ from the wrong surface entirely. Skills shown on both surfaces use ``{{ s_key }}``
12
+ wherever they reference a tool/command so the prose reads naturally to both
13
+ audiences.
14
+
15
+ Face-YAML template tokens such as ``{{ region }}`` (no ``s_`` prefix) pass
16
+ through untouched — the regex only matches the ``s_`` prefix.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import re
22
+ from functools import cache
23
+ from pathlib import Path
24
+ from typing import Literal
25
+
26
+ import yaml
27
+
28
+ SkillSurface = Literal["mcp", "cli"]
29
+
30
+ _ALIASES_PATH = Path(__file__).resolve().parent / "surface_aliases.yaml"
31
+
32
+ # Matches `{{ s_<key> }}`. Keys are lowercase ASCII letters, digits,
33
+ # underscores; must start with a letter. Plain `{{ variable }}` tokens
34
+ # (face-YAML template tokens) do NOT match because they lack the `s_` prefix.
35
+ _MACRO_RE = re.compile(r"\{\{\s*s_([a-z][a-z0-9_]*)\s*\}\}")
36
+
37
+
38
+ @cache
39
+ def _aliases() -> dict[str, dict[str, str]]:
40
+ """Load and validate ``surface_aliases.yaml`` once per process."""
41
+ raw = yaml.safe_load(_ALIASES_PATH.read_text(encoding="utf-8")) or {}
42
+ if not isinstance(raw, dict):
43
+ raise ValueError(f"{_ALIASES_PATH}: top level must be a mapping")
44
+ out: dict[str, dict[str, str]] = {}
45
+ for key, entry in raw.items():
46
+ if not isinstance(entry, dict):
47
+ raise ValueError(f"{_ALIASES_PATH}: entry {key!r} must be a mapping")
48
+ unknown = set(entry) - {"mcp", "dft"}
49
+ if unknown:
50
+ raise ValueError(
51
+ f"{_ALIASES_PATH}: entry {key!r} has unknown keys {sorted(unknown)!r}"
52
+ )
53
+ mcp = entry.get("mcp", "")
54
+ dft = entry.get("dft", "")
55
+ if not isinstance(mcp, str) or not isinstance(dft, str):
56
+ raise ValueError(
57
+ f"{_ALIASES_PATH}: entry {key!r} must have string `mcp` and `dft` values"
58
+ )
59
+ out[key] = {"mcp": mcp, "dft": dft}
60
+ return out
61
+
62
+
63
+ class MissingSurfaceAlias(KeyError):
64
+ """Raised when a SKILL.md references a macro key that has no alias entry."""
65
+
66
+
67
+ def render_skill_body(body: str, *, surface: SkillSurface) -> str:
68
+ """Return ``body`` with ``{{ s_key }}`` macros expanded for ``surface``.
69
+
70
+ Each ``{{ s_key }}`` resolves to the ``mcp`` or ``dft`` side of the alias
71
+ table based on the active surface. Unknown keys raise ``MissingSurfaceAlias``
72
+ so a typo fails the surface fan-out test rather than shipping an unexpanded
73
+ token into an agent's context window.
74
+
75
+ Face-YAML template tokens (``{{ variable }}`` without ``s_`` prefix) pass
76
+ through untouched.
77
+ """
78
+ aliases = _aliases()
79
+ side = "mcp" if surface == "mcp" else "dft"
80
+
81
+ def repl(match: re.Match[str]) -> str:
82
+ key = match.group(1)
83
+ if key not in aliases:
84
+ raise MissingSurfaceAlias(
85
+ f"surface_aliases.yaml has no entry for {key!r} "
86
+ f"(referenced as {{{{ s_{key} }}}})"
87
+ )
88
+ return aliases[key][side]
89
+
90
+ return _MACRO_RE.sub(repl, body)
@@ -0,0 +1,293 @@
1
+ """dataface.agent_api.skills — registry of agent recipes packaged with the wheel.
2
+
3
+ A skill is a directory under ``dataface/ai/skills/<name>/`` containing a
4
+ ``SKILL.md`` file with frontmatter (``name``, ``description``, ``kind``, optional
5
+ ``metadata``, optional ``surfaces``) and an optional ``examples/`` directory of
6
+ face YAML files exercised by the snapshot test in
7
+ ``dataface/tests/agent_api/test_skills.py``.
8
+
9
+ ``kind`` is ``"workflow"`` (end-to-end agent playbooks like ``dashboard-build``)
10
+ or ``"pattern"`` (layout recipes like ``kpi-row``). Required: the loader
11
+ rejects a SKILL.md that omits it so the genre stays self-documenting as new
12
+ skills land.
13
+
14
+ ``surfaces`` is an optional list of ``"mcp"`` / ``"cli"`` declaring where the
15
+ skill should be exposed. Default is both. ``dataface-mcp-setup`` is CLI-only
16
+ because shipping setup instructions to an already-connected MCP agent is
17
+ worse than useless.
18
+
19
+ Skill bodies are authored once with ``{{ s_<key> }}`` macros
20
+ (see ``skill_render.py`` and ``surface_aliases.yaml``); the registry
21
+ renders them on the requested surface before returning.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from functools import cache, cached_property
27
+ from pathlib import Path
28
+ from typing import Literal
29
+
30
+ import yaml
31
+ from pydantic import BaseModel, ConfigDict, Field, computed_field
32
+
33
+ from dataface.agent_api.skill_render import SkillSurface, render_skill_body
34
+
35
+ _SKILLS_DIR = Path(__file__).resolve().parent.parent / "ai" / "skills"
36
+
37
+ SkillKind = Literal["workflow", "pattern"]
38
+ _VALID_KINDS: tuple[SkillKind, ...] = ("workflow", "pattern")
39
+ _VALID_SURFACES: tuple[SkillSurface, ...] = ("mcp", "cli")
40
+ _ALL_SURFACES: frozenset[SkillSurface] = frozenset(_VALID_SURFACES)
41
+
42
+
43
+ class SkillExample(BaseModel):
44
+ path: Path
45
+ name: str # filename without extension
46
+
47
+
48
+ class SkillMetadata(BaseModel):
49
+ author: str | None = None
50
+
51
+
52
+ class Skill(BaseModel):
53
+ name: str
54
+ description: str
55
+ kind: SkillKind
56
+ directory: Path
57
+ body: str
58
+ surfaces: frozenset[SkillSurface] = Field(
59
+ default=_ALL_SURFACES,
60
+ description="Surfaces (`mcp`, `cli`) on which this skill is exposed.",
61
+ )
62
+ rendered_for: SkillSurface | None = Field(
63
+ default=None,
64
+ description=(
65
+ "Surface this body was rendered for. ``None`` on the raw parsed "
66
+ "skill (macros still present); set by ``list_skills`` / "
67
+ "``get_skill`` / ``search_skills`` to the surface they rendered."
68
+ ),
69
+ )
70
+ examples: list[SkillExample] = Field(
71
+ default_factory=list, description="Example face YAML entries for this skill."
72
+ )
73
+ metadata: SkillMetadata = Field(
74
+ default_factory=SkillMetadata,
75
+ description="Parsed SKILL.md frontmatter metadata.",
76
+ )
77
+
78
+ @computed_field # type: ignore[prop-decorator]
79
+ @cached_property
80
+ def has_examples(self) -> bool:
81
+ return bool(self.examples)
82
+
83
+
84
+ class SkillList(BaseModel):
85
+ success: bool = True
86
+ skills: list[Skill] = Field(
87
+ default_factory=list, description="Available skills parsed from disk."
88
+ )
89
+ errors: list[str] = Field(
90
+ default_factory=list, description="Errors encountered while loading skills."
91
+ )
92
+
93
+
94
+ class SkillSearchHit(BaseModel):
95
+ name: str
96
+ description: str
97
+ kind: SkillKind
98
+ has_examples: bool
99
+ score: float
100
+
101
+
102
+ class SkillSearchResult(BaseModel):
103
+ success: bool = True
104
+ query: str
105
+ hits: list[SkillSearchHit] = Field(default_factory=list)
106
+
107
+
108
+ class SkillNotFound(Exception): ...
109
+
110
+
111
+ class GetSkillArgs(BaseModel):
112
+ """Input to get_skill: fetch one skill by name."""
113
+
114
+ name: str = Field(..., description="Skill directory name (kebab-case)")
115
+
116
+
117
+ class SearchSkillsArgs(BaseModel):
118
+ """Input to search_skills: substring search across names, descriptions, and bodies."""
119
+
120
+ query: str = Field(..., description="Substring query (case-insensitive)")
121
+ limit: int = Field(10, ge=1, le=25, description="Max hits to return")
122
+
123
+ model_config = ConfigDict(extra="forbid")
124
+
125
+
126
+ def _parse_surfaces(value: object, skill_md: Path) -> frozenset[SkillSurface]:
127
+ """Validate the optional ``surfaces:`` frontmatter list."""
128
+ if value is None:
129
+ return _ALL_SURFACES
130
+ if not isinstance(value, list) or not value:
131
+ raise ValueError(
132
+ f"{skill_md}: `surfaces` must be a non-empty list (got {value!r})"
133
+ )
134
+ out: set[SkillSurface] = set()
135
+ for item in value:
136
+ if item not in _VALID_SURFACES:
137
+ raise ValueError(
138
+ f"{skill_md}: `surfaces` entry {item!r} not in {_VALID_SURFACES}"
139
+ )
140
+ out.add(item)
141
+ return frozenset(out)
142
+
143
+
144
+ def _parse_skill_dir(directory: Path) -> Skill:
145
+ """Parse a single skill directory into a Skill. Raises ValueError on bad data."""
146
+ skill_md = directory / "SKILL.md"
147
+ raw = skill_md.read_text(encoding="utf-8")
148
+
149
+ # Split frontmatter: expect leading --- block
150
+ if not raw.startswith("---"):
151
+ raise ValueError(f"{skill_md}: missing frontmatter (no leading ---)")
152
+ parts = raw.split("---", 2)
153
+ if len(parts) < 3:
154
+ raise ValueError(f"{skill_md}: malformed frontmatter")
155
+ frontmatter = yaml.safe_load(parts[1]) or {}
156
+ body = parts[2].lstrip("\n")
157
+
158
+ name = frontmatter.get("name")
159
+ if not name:
160
+ raise ValueError(f"{skill_md}: frontmatter missing 'name'")
161
+ if name != directory.name:
162
+ raise ValueError(
163
+ f"{skill_md}: frontmatter name {name!r} does not match directory name {directory.name!r}"
164
+ )
165
+
166
+ description = frontmatter.get("description", "")
167
+ if not description:
168
+ raise ValueError(f"{skill_md}: frontmatter missing 'description'")
169
+
170
+ kind = frontmatter.get("kind")
171
+ if not kind:
172
+ raise ValueError(
173
+ f"{skill_md}: frontmatter missing 'kind' (must be one of {_VALID_KINDS})"
174
+ )
175
+ if kind not in _VALID_KINDS:
176
+ raise ValueError(
177
+ f"{skill_md}: invalid kind {kind!r}; must be one of {_VALID_KINDS}"
178
+ )
179
+
180
+ meta_raw = frontmatter.get("metadata") or {}
181
+ metadata = SkillMetadata(author=meta_raw.get("author"))
182
+
183
+ surfaces = _parse_surfaces(frontmatter.get("surfaces"), skill_md)
184
+
185
+ examples: list[SkillExample] = []
186
+ examples_dir = directory / "examples"
187
+ if examples_dir.is_dir():
188
+ for yml_path in sorted(examples_dir.glob("*.yml")):
189
+ examples.append(SkillExample(path=yml_path, name=yml_path.stem))
190
+
191
+ return Skill(
192
+ name=name,
193
+ description=description.strip(),
194
+ kind=kind,
195
+ directory=directory,
196
+ body=body,
197
+ surfaces=surfaces,
198
+ examples=examples,
199
+ metadata=metadata,
200
+ )
201
+
202
+
203
+ def all_skill_names() -> frozenset[str]:
204
+ """Every skill directory name shipped in the wheel."""
205
+ return frozenset(_load_all())
206
+
207
+
208
+ @cache
209
+ def _load_all() -> dict[str, Skill]:
210
+ """Walk _SKILLS_DIR and parse every <name>/SKILL.md. Cached for process lifetime."""
211
+ skills: dict[str, Skill] = {}
212
+ for skill_md in sorted(_SKILLS_DIR.glob("*/SKILL.md")):
213
+ directory = skill_md.parent
214
+ skill = _parse_skill_dir(directory)
215
+ skills[skill.name] = skill
216
+ return skills
217
+
218
+
219
+ def _render_for_surface(skill: Skill, surface: SkillSurface) -> Skill:
220
+ """Return a shallow copy of ``skill`` with body + ``rendered_for`` set."""
221
+ return skill.model_copy(
222
+ update={
223
+ "body": render_skill_body(skill.body, surface=surface),
224
+ "rendered_for": surface,
225
+ }
226
+ )
227
+
228
+
229
+ def list_skills(*, surface: SkillSurface = "mcp") -> SkillList:
230
+ """Return every skill exposed on ``surface``, with bodies rendered for it."""
231
+ skills = [
232
+ _render_for_surface(s, surface)
233
+ for s in _load_all().values()
234
+ if surface in s.surfaces
235
+ ]
236
+ skills.sort(key=lambda s: s.name)
237
+ return SkillList(skills=skills)
238
+
239
+
240
+ def get_skill(name: str, *, surface: SkillSurface = "mcp") -> Skill:
241
+ """Return one skill by directory name, rendered for ``surface``.
242
+
243
+ Raises ``SkillNotFound`` when the skill does not exist *or* is not exposed
244
+ on the requested surface (e.g. ``dataface-mcp-setup`` is CLI-only and is
245
+ invisible to MCP callers).
246
+ """
247
+ skills = _load_all()
248
+ skill = skills.get(name)
249
+ if skill is None or surface not in skill.surfaces:
250
+ raise SkillNotFound(
251
+ f"Unknown skill: {name!r}. Run `dft skills` to list available."
252
+ )
253
+ return _render_for_surface(skill, surface)
254
+
255
+
256
+ def search_skills(
257
+ query: str, *, limit: int = 10, surface: SkillSurface = "mcp"
258
+ ) -> SkillSearchResult:
259
+ """Substring search across name (1.0), description (0.8), and rendered body (0.5).
260
+
261
+ Honors the per-skill ``surfaces`` frontmatter — a skill that isn't exposed
262
+ on ``surface`` is invisible to search on that surface. Body matches use the
263
+ surface-rendered body so macro expansion doesn't leak macro suffixes into
264
+ relevance ranking.
265
+ """
266
+ q = query.strip().lower()
267
+ if not q:
268
+ raise ValueError("query must be a non-empty string")
269
+
270
+ hits: list[SkillSearchHit] = []
271
+ for skill in _load_all().values():
272
+ if surface not in skill.surfaces:
273
+ continue
274
+ if q in skill.name.lower():
275
+ score = 1.0
276
+ elif q in skill.description.lower():
277
+ score = 0.8
278
+ elif q in render_skill_body(skill.body, surface=surface).lower():
279
+ score = 0.5
280
+ else:
281
+ continue
282
+ hits.append(
283
+ SkillSearchHit(
284
+ name=skill.name,
285
+ description=skill.description,
286
+ kind=skill.kind,
287
+ has_examples=skill.has_examples,
288
+ score=score,
289
+ )
290
+ )
291
+
292
+ hits.sort(key=lambda h: (-h.score, h.name))
293
+ return SkillSearchResult(query=query, hits=hits[:limit])
@@ -0,0 +1,128 @@
1
+ # Surface aliases for skill macro expansion.
2
+ #
3
+ # Authoring convention:
4
+ #
5
+ # {{ s_<key> }} renders as the `mcp` side on MCP surface, `dft` side on CLI
6
+ #
7
+ # Common patterns inside a SKILL.md:
8
+ #
9
+ # "Use `{{ s_validate_dashboard }}` after every edit."
10
+ # → MCP agent reads: `validate_dashboard`
11
+ # → CLI agent reads: `dft validate`
12
+ #
13
+ # "{{ s_render_png_example }}"
14
+ # → Whole code block flips between surfaces.
15
+ #
16
+ # Each top-level key here maps to an `mcp:` and `dft:` value. Either side may
17
+ # be an empty string when the content is one-sided.
18
+ #
19
+ # Add a new key here, then reference it as `{{ s_<key> }}` in any wheel
20
+ # SKILL.md. Face-YAML template tokens (`{{ region }}` etc.) are not macros —
21
+ # they have no `s_` prefix and pass through untouched. Test coverage lives in
22
+ # `dataface/tests/agent_api/test_skill_render.py` and the surface fan-out test
23
+ # in `dataface/tests/agent_api/test_skills.py`.
24
+
25
+ # ---- Inline tool references (one-liners inside prose) ----------------------
26
+
27
+ validate_dashboard:
28
+ mcp: "validate_dashboard"
29
+ dft: "dft validate"
30
+
31
+ execute_query:
32
+ mcp: "execute_query"
33
+ dft: "dft query"
34
+
35
+ render_dashboard:
36
+ mcp: "render_dashboard"
37
+ dft: "dft render"
38
+
39
+ query_face:
40
+ mcp: "query_face"
41
+ dft: "dft query --face"
42
+
43
+ describe_query:
44
+ mcp: "describe_query"
45
+ dft: "dft query --describe"
46
+
47
+ schema:
48
+ mcp: "schema"
49
+ dft: "dft schema"
50
+
51
+ schema_search:
52
+ mcp: "schema_search"
53
+ dft: "dft schema -s"
54
+
55
+ # ---- Inline skill-pointer references --------------------------------------
56
+
57
+ skill_build:
58
+ mcp: "get_skill('dashboard-build')"
59
+ dft: "dft skills dashboard-build"
60
+
61
+ skill_design_dashboard:
62
+ mcp: "get_skill('dashboard-design')"
63
+ dft: "dft skills dashboard-design"
64
+
65
+ skill_design_report:
66
+ mcp: "get_skill('report-design')"
67
+ dft: "dft skills report-design"
68
+
69
+ skill_review:
70
+ mcp: "get_skill('dashboard-review')"
71
+ dft: "dft skills dashboard-review"
72
+
73
+ skill_troubleshooting:
74
+ mcp: "get_skill('dataface-troubleshooting')"
75
+ dft: "dft skills dataface-troubleshooting"
76
+
77
+ # ---- Sentence-level shared phrases (same intent, different vocabulary) ----
78
+
79
+ render_to_verify:
80
+ mcp: "Call `render_dashboard` to see the visual result — queries are cached after first execution, so re-rendering is nearly instant."
81
+ dft: "Run `dft render <path>` to see the visual result — queries are cached after first execution, so re-rendering is nearly instant."
82
+
83
+ explore_schema_first:
84
+ mcp: "Use `schema` to drill source → schema → table → column, and `schema_search` for keyword search across the corpus."
85
+ dft: "Use `dft schema` to drill source → schema → table → column, and `dft schema -s <kw>` for keyword search across the corpus."
86
+
87
+ # ---- Discovery / footer blocks (whole-paragraph, both sides populated) ----
88
+
89
+ docs_discovery:
90
+ mcp: |
91
+ YAML field reference lives behind the `docs` tool (overview),
92
+ `docs(topic=…)` (one section in depth), and `docs(search=…)` (full-text
93
+ search).
94
+ dft: |
95
+ YAML field reference is `dft docs` (overview), `dft docs <topic>` (one
96
+ section in depth), and `dft docs -s "<query>"` (full-text search).
97
+
98
+ # ---- Code-block examples (whole fenced block, surface-specific) -----------
99
+
100
+ render_png_example:
101
+ mcp: |
102
+ ```text
103
+ render_dashboard(path="faces/finance/revenue-overview.yml", format="png")
104
+ ```
105
+ dft: |
106
+ ```bash
107
+ dft render faces/finance/revenue-overview.yml --format png
108
+ ```
109
+
110
+ validate_example:
111
+ mcp: |
112
+ ```text
113
+ validate_dashboard(path="faces/finance/revenue-overview.yml")
114
+ ```
115
+ dft: |
116
+ ```bash
117
+ dft validate faces/finance/revenue-overview.yml
118
+ ```
119
+
120
+ # ---- Cross-skill pointers (single-line footers) ---------------------------
121
+
122
+ yaml_reference_footer:
123
+ mcp: "Full YAML reference is the `docs` tool — call `docs` for the overview, `docs(topic=…)` for one section, `docs(search=…)` for full-text search."
124
+ dft: "Full YAML reference is `dft docs` — `dft docs` for the overview, `dft docs <topic>` for one section, `dft docs -s \"<query>\"` for full-text search."
125
+
126
+ review_pointer:
127
+ mcp: "Run `get_skill('dashboard-review')` before declaring a face done."
128
+ dft: "Run `dft skills dashboard-review` before declaring a face done."
@@ -0,0 +1,175 @@
1
+ """Typed validate verb — fast YAML schema + cross-reference validation.
2
+
3
+ No warehouse connection, no query execution.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+ from dataface.agent_api._paths import (
13
+ no_project_hint,
14
+ project_root_for,
15
+ resolve_scoped_path,
16
+ )
17
+ from dataface.core.compile.errors import DatafaceError
18
+ from dataface.core.errors import DF_UNKNOWN_INTERNAL, StructuredError
19
+ from dataface.core.render.warnings.base import RenderWarning
20
+
21
+
22
+ class ValidateDashboardArgs(BaseModel):
23
+ """Validate a face YAML file without executing queries.
24
+
25
+ Runs YAML parse, Pydantic schema validation, and cross-reference checks
26
+ (chart → query resolution, variable resolution, partial-include expansion).
27
+ Fast — no warehouse connection, no query execution. Use this as the inner
28
+ edit-loop verb: edit, validate, fix, repeat. Prefer render_dashboard for a
29
+ full compile + execute validation.
30
+ """
31
+
32
+ path: Path = Field(
33
+ ..., description="Path to face YAML file (relative to project_dir or cwd)"
34
+ )
35
+ project_dir: Path | None = Field(
36
+ None, description="Project root for resolving relative paths"
37
+ )
38
+
39
+
40
+ class ValidateResult(BaseModel):
41
+ success: bool
42
+ path: Path
43
+ errors: list[StructuredError] = Field(
44
+ default_factory=list, description="Validation errors found in the face file."
45
+ )
46
+ warnings: list[RenderWarning] = Field(
47
+ default_factory=list,
48
+ description=(
49
+ "Non-fatal warnings as RenderWarning objects with stable codes. "
50
+ "Each warning carries code, message, and an optional fix hint."
51
+ ),
52
+ )
53
+
54
+
55
+ def validate_paths(
56
+ paths: list[Path] | None,
57
+ project_dir: Path | None = None,
58
+ ) -> list[ValidateResult]:
59
+ """Validate N face files / directories, or default ``faces/`` when empty.
60
+
61
+ Each argv path expands like the single-path verb did: a file validates
62
+ directly, a directory walks recursively (skipping ``_*.yml`` partials and
63
+ ejected inspect-template directories). Empty / ``None`` defaults to
64
+ ``<project_root>/faces``. Results are concatenated in argv order.
65
+ """
66
+ if not paths:
67
+ return _validate_one_path(None, project_dir=project_dir)
68
+ out: list[ValidateResult] = []
69
+ for p in paths:
70
+ out.extend(_validate_one_path(p, project_dir=project_dir))
71
+ return out
72
+
73
+
74
+ def _validate_one_path(
75
+ path: Path | None,
76
+ project_dir: Path | None = None,
77
+ ) -> list[ValidateResult]:
78
+ """Per-argv expansion: ``None`` → faces/, file → [one], dir → walk."""
79
+ # WHY: dataface.core.inspect.manifest_utils triggers the inspect package
80
+ # __init__, which eagerly imports TableInspector + grain/quality/semantic
81
+ # detectors. Keep this lazy so `dft --help` doesn't pay that startup cost.
82
+ from dataface.core.inspect.manifest_utils import INSPECT_TEMPLATE_MANIFEST
83
+
84
+ raw_path = path if path is not None else project_root_for(project_dir) / "faces"
85
+
86
+ try:
87
+ resolved = resolve_scoped_path(raw_path, project_dir)
88
+ except ValueError as exc:
89
+ return [
90
+ ValidateResult(
91
+ success=False,
92
+ path=raw_path,
93
+ errors=[
94
+ DatafaceError.from_code(
95
+ DF_UNKNOWN_INTERNAL, message=str(exc)
96
+ ).to_structured()
97
+ ],
98
+ )
99
+ ]
100
+
101
+ if not resolved.is_dir():
102
+ return [validate(resolved, project_dir=project_dir)]
103
+
104
+ template_dirs = {m.parent for m in resolved.glob(f"**/{INSPECT_TEMPLATE_MANIFEST}")}
105
+ yaml_files = sorted(
106
+ f
107
+ for f in (list(resolved.glob("**/*.yml")) + list(resolved.glob("**/*.yaml")))
108
+ if not f.name.startswith("_") and f.parent not in template_dirs
109
+ )
110
+ if not yaml_files:
111
+ return [
112
+ ValidateResult(
113
+ success=False,
114
+ path=resolved,
115
+ errors=[
116
+ DatafaceError.from_code(
117
+ DF_UNKNOWN_INTERNAL,
118
+ message=f"No face files found in {resolved}",
119
+ ).to_structured()
120
+ ],
121
+ )
122
+ ]
123
+ return [validate(f, project_dir=project_dir) for f in yaml_files]
124
+
125
+
126
+ def validate(path: Path, project_dir: Path | None = None) -> ValidateResult:
127
+ """Fast YAML schema + cross-reference validation. No warehouse, no execute."""
128
+ from dataface.core.compile.compiler import compile_file
129
+
130
+ try:
131
+ resolved = resolve_scoped_path(path, project_dir)
132
+ except ValueError as exc:
133
+ return ValidateResult(
134
+ success=False,
135
+ path=path,
136
+ errors=[
137
+ DatafaceError.from_code(
138
+ DF_UNKNOWN_INTERNAL, message=str(exc)
139
+ ).to_structured()
140
+ ],
141
+ )
142
+
143
+ if not resolved.exists():
144
+ hint = no_project_hint(project_dir)
145
+ return ValidateResult(
146
+ success=False,
147
+ path=path,
148
+ errors=[
149
+ DatafaceError.from_code(
150
+ DF_UNKNOWN_INTERNAL,
151
+ message=f"File not found: {resolved}{hint}",
152
+ ).to_structured()
153
+ ],
154
+ )
155
+
156
+ try:
157
+ result = compile_file(resolved)
158
+ except OSError as exc:
159
+ return ValidateResult(
160
+ success=False,
161
+ path=resolved,
162
+ errors=[
163
+ DatafaceError.from_code(
164
+ DF_UNKNOWN_INTERNAL, message=str(exc)
165
+ ).to_structured(file=str(resolved))
166
+ ],
167
+ )
168
+
169
+ errors = list(result.errors)
170
+ return ValidateResult(
171
+ success=len(errors) == 0,
172
+ path=resolved,
173
+ errors=errors,
174
+ warnings=list(result.warnings),
175
+ )