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,284 @@
1
+ """Typed schema_search verb — full-text + filter over the metadata corpus.
2
+
3
+ Companion to ``dft schema``: the drill-down verb answers "I know what I'm
4
+ looking for" (navigation), this verb answers "I have a property, find what
5
+ matches" (find/filter). The two output shapes are intentionally different:
6
+ ``schema`` returns a hierarchical named-dict tree wrapped in ``sources``;
7
+ ``schema_search`` returns a flat list of fully-qualified hits.
8
+
9
+ Provenance flows through unchanged from the resolver: a hit on a name that
10
+ came from the adapter carries ``source='dbt_adapter'``; a hit on a
11
+ description that came from the cache carries ``source='super_schema'``;
12
+ a hit on a tag/test/owner from the manifest carries
13
+ ``source='dbt_manifest'``.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from datetime import datetime, timezone
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
23
+
24
+ from dataface.core.execute.adapters import AdapterRegistry
25
+ from dataface.core.inspect.cache_factory import build_resolver
26
+ from dataface.core.inspect.search import InvalidRegex, SchemaSearch, SchemaSearchHit
27
+
28
+
29
+ class SchemaSearchArgs(BaseModel):
30
+ """Search the company's data vocabulary by keyword + structured filters.
31
+
32
+ Returns a flat list of fully-qualified hits across the metadata corpus:
33
+ schema/table/column names, descriptions (column + table), tags, listed
34
+ tests, and meta values. Use this when you want to find vocabulary or
35
+ cross-cutting matches ("which tables have a customer_id column?",
36
+ "find columns tagged pii", "find primary keys in analytics") — not for
37
+ drilling into a specific known table (use ``schema`` for that).
38
+
39
+ Keyword is a case-insensitive substring match by default; pass
40
+ ``regex=True`` for a Python regex. Scope narrows which text-bearing
41
+ fields are searched (default: all of name, description, tag, tests,
42
+ meta, owner). Structured filters (``role``, ``tag``, ``has_test``,
43
+ ``missing``, ``column_name``, ``table_name``, ``meta``, ``fk_to``) layer
44
+ as additional predicates.
45
+
46
+ Hits are returned in stable iteration order — no ranking, no top-N.
47
+ Each hit reports the path keys via ``location`` ("source.schema.table"
48
+ or "source.schema.table.column"), which text-bearing field matched
49
+ (``matched_field``), an excerpt (``snippet``), and the layer that
50
+ contributed the field (``source`` — "super_schema", "dbt_manifest",
51
+ or "dbt_adapter").
52
+ """
53
+
54
+ model_config = ConfigDict(populate_by_name=True)
55
+
56
+ keyword: str = Field(
57
+ "",
58
+ description=(
59
+ "Substring to match (case-insensitive). Empty string disables "
60
+ "keyword matching — use with structured filters (role, tag, "
61
+ "has_test, missing, ...) to enumerate qualifying rows."
62
+ ),
63
+ )
64
+ scope: list[str] | None = Field(
65
+ None,
66
+ description=(
67
+ "Limit which text-bearing fields are searched. "
68
+ "Choices: name, description, tag, tests, meta, owner. "
69
+ "Default: all."
70
+ ),
71
+ )
72
+ regex: bool = Field(
73
+ False,
74
+ description="Treat keyword as a Python regex (case-insensitive).",
75
+ )
76
+ role: str | None = Field(
77
+ None,
78
+ description=(
79
+ "Filter columns by profiled role "
80
+ "(primary_key, foreign_key, dimension, measure, ...)."
81
+ ),
82
+ )
83
+ tag: str | None = Field(
84
+ None,
85
+ description="Filter to rows with this tag in their tags array.",
86
+ )
87
+ has_test: str | None = Field(
88
+ None,
89
+ description=(
90
+ "Filter columns whose tests include this name "
91
+ "(unique, not_null, accepted_values, relationships, ...)."
92
+ ),
93
+ )
94
+ missing: str | None = Field(
95
+ None,
96
+ description=(
97
+ "Emit synthetic hits for rows that LACK this field "
98
+ "(description, tag, owner, ...). matched_field on each hit is "
99
+ "'_missing:<field>'."
100
+ ),
101
+ )
102
+ meta: dict[str, str] | None = Field(
103
+ None,
104
+ description="Filter by exact-match meta key=value pairs.",
105
+ )
106
+ column_name: str | None = Field(
107
+ None,
108
+ description=(
109
+ "Filter to columns whose name matches this fnmatch glob "
110
+ "(e.g. *_id, customer_*)."
111
+ ),
112
+ )
113
+ table_name: str | None = Field(
114
+ None,
115
+ description=(
116
+ "Filter to tables whose name matches this fnmatch glob "
117
+ "(e.g. stg_*, fct_*)."
118
+ ),
119
+ )
120
+ fk_to: str | None = Field(
121
+ None,
122
+ description=(
123
+ "Filter to columns with an explicit relationships: test "
124
+ "pointing at this table. No naming-heuristic inference."
125
+ ),
126
+ )
127
+ fields: list[str] | None = Field(
128
+ None,
129
+ description=(
130
+ "Project each hit to a subset of fields "
131
+ "(location, matched_field, snippet, source, value, attrs)."
132
+ ),
133
+ )
134
+ limit: int | None = Field(
135
+ None,
136
+ description=(
137
+ "Truncate results to N hits. ``total`` and ``truncated`` "
138
+ "report the pre-truncation count."
139
+ ),
140
+ )
141
+
142
+
143
+ class _SearchMeta(BaseModel):
144
+ """Single ``_meta`` footer per response."""
145
+
146
+ retrieved_at: str
147
+
148
+
149
+ class SchemaSearchResult(BaseModel):
150
+ """Flat list of hits across the metadata corpus."""
151
+
152
+ model_config = ConfigDict(populate_by_name=True)
153
+
154
+ success: bool = True
155
+ hits: list[SchemaSearchHit] = Field(
156
+ default_factory=list,
157
+ description="Matching schema entries, ranked by relevance.",
158
+ )
159
+ total: int = 0
160
+ truncated: bool = False
161
+ # Per-source schema-walk failures (unreachable adapter, etc.). Surfaced
162
+ # so callers can tell "no hits" apart from "search couldn't reach a source"
163
+ # — the silent-fallback antipattern AGENTS.md flags.
164
+ warnings: list[str] = Field(
165
+ default_factory=list,
166
+ description="Per-source warnings (e.g. unreachable adapter).",
167
+ )
168
+ meta: _SearchMeta | None = Field(
169
+ default=None, alias="_meta", description="Pagination and timing metadata."
170
+ )
171
+ errors: list[str] = Field(
172
+ default_factory=list, description="Errors encountered during search."
173
+ )
174
+
175
+ # Projection is applied at dump time — not part of the schema. CLI/MCP
176
+ # callers that pass `fields=[...]` get a lean wire shape; in-process
177
+ # consumers iterate the typed `hits` list with full fields.
178
+ _project_fields: list[str] | None = PrivateAttr(default=None)
179
+
180
+ def model_dump(self, **kwargs: Any) -> dict[str, Any]:
181
+ out = super().model_dump(**kwargs)
182
+ if self._project_fields is not None:
183
+ allowed = set(self._project_fields)
184
+ out["hits"] = [
185
+ {k: v for k, v in hit.items() if k in allowed} for hit in out["hits"]
186
+ ]
187
+ return out
188
+
189
+ def model_dump_json(self, **kwargs: Any) -> str:
190
+ # model_dump_json bypasses model_dump — re-route through dump+json.dumps
191
+ # so projection is honored. Force ``mode='json'`` so any future field
192
+ # carrying a non-JSON-native value (datetime, Decimal, Path) is
193
+ # converted by Pydantic, not crashed by stdlib json.dumps. ``indent``
194
+ # is a json.dumps-only kwarg; pop it out of the model_dump call.
195
+ import json as _json
196
+
197
+ indent = kwargs.pop("indent", None)
198
+ kwargs.setdefault("mode", "json")
199
+ return _json.dumps(self.model_dump(**kwargs), indent=indent)
200
+
201
+
202
+ def schema_search(
203
+ keyword: str = "",
204
+ *,
205
+ scope: list[str] | None = None,
206
+ regex: bool = False,
207
+ role: str | None = None,
208
+ tag: str | None = None,
209
+ has_test: str | None = None,
210
+ missing: str | None = None,
211
+ meta: dict[str, str] | None = None,
212
+ column_name: str | None = None,
213
+ table_name: str | None = None,
214
+ fk_to: str | None = None,
215
+ fields: list[str] | None = None,
216
+ limit: int | None = None,
217
+ cache_path: Path | None = None,
218
+ adapter_registry: AdapterRegistry,
219
+ ) -> SchemaSearchResult:
220
+ """Full-text + filter search across the resolver's materialized schema search index."""
221
+ if (
222
+ not keyword
223
+ and missing is None
224
+ and not _any_filter(role, tag, has_test, meta, column_name, table_name, fk_to)
225
+ ):
226
+ return SchemaSearchResult(
227
+ success=False,
228
+ errors=[
229
+ "schema_search requires a keyword or at least one filter "
230
+ "(role, tag, has_test, missing, meta, column_name, table_name, fk_to)"
231
+ ],
232
+ )
233
+ try:
234
+ resolver = build_resolver(adapter_registry, cache_path)
235
+ searcher = SchemaSearch(resolver)
236
+ hits, warnings = searcher.search(
237
+ keyword,
238
+ scope=scope,
239
+ regex=regex,
240
+ role=role,
241
+ tag=tag,
242
+ has_test=has_test,
243
+ missing=missing,
244
+ meta=meta,
245
+ column_name=column_name,
246
+ table_name=table_name,
247
+ fk_to=fk_to,
248
+ )
249
+ except InvalidRegex as exc:
250
+ return SchemaSearchResult(success=False, errors=[str(exc)])
251
+ except Exception as exc: # noqa: BLE001 — verb returns errors via response envelope
252
+ return SchemaSearchResult(success=False, errors=[str(exc)])
253
+
254
+ total = len(hits)
255
+ truncated = False
256
+ if limit is not None and total > limit:
257
+ hits = hits[:limit]
258
+ truncated = True
259
+
260
+ result = SchemaSearchResult(
261
+ hits=hits,
262
+ total=total,
263
+ truncated=truncated,
264
+ warnings=warnings,
265
+ _meta=_SearchMeta(retrieved_at=datetime.now(timezone.utc).isoformat()),
266
+ )
267
+ if fields is not None:
268
+ result._project_fields = list(fields)
269
+ return result
270
+
271
+
272
+ def _any_filter(
273
+ role: str | None,
274
+ tag: str | None,
275
+ has_test: str | None,
276
+ meta: dict[str, str] | None,
277
+ column_name: str | None,
278
+ table_name: str | None,
279
+ fk_to: str | None,
280
+ ) -> bool:
281
+ return any(
282
+ f is not None
283
+ for f in (role, tag, has_test, meta, column_name, table_name, fk_to)
284
+ )
@@ -0,0 +1,270 @@
1
+ """Typed search verb for the agent API."""
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import yaml
8
+ from pydantic import BaseModel, ConfigDict, Field
9
+
10
+ MAX_SEARCH_LIMIT = 50
11
+ DEFAULT_SEARCH_LIMIT = 10
12
+
13
+ # Maintainer paths that must not appear in agent-facing search summaries.
14
+ _INTERNAL_REF_RE = re.compile(r"\s*;?\s*see\s+ai_notes/\S+", re.IGNORECASE)
15
+
16
+
17
+ def _agent_safe_summary(description: str) -> str:
18
+ """Strip maintainer-only references from face descriptions."""
19
+ return _INTERNAL_REF_RE.sub("", description).strip()
20
+
21
+
22
+ class SearchDashboardsArgs(BaseModel):
23
+ """Search existing dashboards by keyword. Returns ranked results with match scores and reasons. Use to discover relevant dashboards and reuse validated query patterns before creating new ones. The returned source_path can be passed to render_dashboard(path=source_path, ...) to inspect that dashboard. Results may also include sample SQL and literal file paths from matching queries; reuse those exact paths instead of inventing new file globs."""
24
+
25
+ model_config = ConfigDict(extra="forbid")
26
+
27
+ query: str = Field(
28
+ ...,
29
+ description="Search query text (keywords to match against dashboard metadata)",
30
+ )
31
+ project_dir: Path | None = Field(
32
+ None,
33
+ description="Project directory to search (defaults to the dispatch context's dashboards directory, then CWD)",
34
+ )
35
+ tags: list[str] | None = Field(
36
+ None, description="Filter results to dashboards with ALL of these tags"
37
+ )
38
+ limit: int | None = Field(
39
+ None, description="Maximum results to return (default 10, max 50)"
40
+ )
41
+
42
+
43
+ _index_cache: list[dict[str, Any]] | None = None
44
+ _index_cache_dir: Path | None = None
45
+ _index_cache_file_set: set[Path] | None = None
46
+
47
+
48
+ class DashboardSearchHit(BaseModel):
49
+ title: str
50
+ summary: str
51
+ match_score: float
52
+ match_reasons: list[str]
53
+ source_path: Path
54
+ query_names: list[str]
55
+ chart_names: list[str]
56
+ sample_sql: str | None = None
57
+ file_paths: list[str]
58
+
59
+
60
+ class SearchResult(BaseModel):
61
+ success: bool
62
+ errors: list[str]
63
+ results: list[DashboardSearchHit]
64
+
65
+
66
+ def search_dashboards(
67
+ query: str,
68
+ project_dir: Path | None = None,
69
+ tags: list[str] | None = None,
70
+ limit: int = DEFAULT_SEARCH_LIMIT,
71
+ ) -> SearchResult:
72
+ """Search dashboards by keyword with ranked results."""
73
+ limit = max(0, min(limit, MAX_SEARCH_LIMIT))
74
+ query_stripped = query.strip()
75
+
76
+ if not query_stripped or limit == 0:
77
+ return SearchResult(success=True, errors=[], results=[])
78
+
79
+ query_tokens = list(_tokenize(query_stripped))
80
+ if not query_tokens:
81
+ return SearchResult(success=True, errors=[], results=[])
82
+
83
+ index = _get_index(project_dir or Path("."))
84
+
85
+ if tags:
86
+ required_tags = {t.lower() for t in tags}
87
+ index = [e for e in index if required_tags.issubset(set(e["tags"]))]
88
+
89
+ scored: list[tuple[float, Path, dict[str, Any], list[str]]] = []
90
+ for entry in index:
91
+ score, reasons = _score_entry(entry, query_tokens)
92
+ if score > 0:
93
+ scored.append((score, entry["source_path"], entry, reasons))
94
+
95
+ scored.sort(key=lambda x: (-x[0], str(x[1])))
96
+
97
+ results = []
98
+ for score, _path, entry, reasons in scored[:limit]:
99
+ results.append(
100
+ DashboardSearchHit(
101
+ title=entry["title"],
102
+ summary=entry["description"],
103
+ match_score=round(score, 2),
104
+ match_reasons=reasons,
105
+ source_path=entry["source_path"],
106
+ query_names=entry["query_names"],
107
+ chart_names=entry["chart_names"],
108
+ sample_sql=(
109
+ entry["sql_snippets"][0] if entry["sql_snippets"] else None
110
+ ),
111
+ file_paths=_extract_file_paths(entry["sql_snippets"]),
112
+ )
113
+ )
114
+
115
+ return SearchResult(success=True, errors=[], results=results)
116
+
117
+
118
+ def _build_index(directory: Path) -> list[dict[str, Any]]:
119
+ from dataface.agent_api.dashboards import list_dashboards
120
+
121
+ listing = list_dashboards(directory, recursive=True)
122
+ entries: list[dict[str, Any]] = []
123
+
124
+ for dash in listing.dashboards:
125
+ abs_path = dash.absolute_path
126
+ try:
127
+ content = yaml.safe_load(abs_path.read_text())
128
+ except (yaml.YAMLError, OSError):
129
+ continue
130
+
131
+ if not isinstance(content, dict):
132
+ continue
133
+
134
+ tags = content.get("tags", [])
135
+ queries = content.get("queries", {})
136
+ charts = content.get("charts", {})
137
+
138
+ sql_snippets: list[str] = []
139
+ query_names: list[str] = []
140
+ if isinstance(queries, dict):
141
+ for qname, qdef in queries.items():
142
+ query_names.append(qname)
143
+ if isinstance(qdef, dict) and qdef.get("sql"):
144
+ sql_snippets.append(qdef["sql"])
145
+
146
+ chart_names: list[str] = []
147
+ if isinstance(charts, dict):
148
+ chart_names = list(charts.keys())
149
+
150
+ entries.append(
151
+ {
152
+ "source_path": dash.path,
153
+ "title": dash.title,
154
+ "description": _agent_safe_summary(dash.description),
155
+ "tags": [t.lower() for t in tags] if isinstance(tags, list) else [],
156
+ "query_names": query_names,
157
+ "chart_names": chart_names,
158
+ "sql_snippets": sql_snippets,
159
+ "mtime": abs_path.stat().st_mtime,
160
+ }
161
+ )
162
+
163
+ return entries
164
+
165
+
166
+ def _get_index(directory: Path) -> list[dict[str, Any]]:
167
+ global _index_cache, _index_cache_dir, _index_cache_file_set
168
+
169
+ dir_path = directory.resolve()
170
+
171
+ if _index_cache is not None and _index_cache_dir == dir_path:
172
+ current_files = {
173
+ f.relative_to(dir_path)
174
+ for f in dir_path.glob("**/*.yml")
175
+ if not f.name.startswith("_")
176
+ }
177
+ current_files |= {
178
+ f.relative_to(dir_path)
179
+ for f in dir_path.glob("**/*.yaml")
180
+ if not f.name.startswith("_")
181
+ }
182
+
183
+ if current_files == _index_cache_file_set:
184
+ still_valid = True
185
+ for entry in _index_cache:
186
+ fpath = dir_path / entry["source_path"]
187
+ try:
188
+ if fpath.stat().st_mtime != entry["mtime"]:
189
+ still_valid = False
190
+ break
191
+ except OSError:
192
+ still_valid = False
193
+ break
194
+ if still_valid:
195
+ return _index_cache
196
+
197
+ snapshot = {
198
+ f.relative_to(dir_path)
199
+ for f in dir_path.glob("**/*.yml")
200
+ if not f.name.startswith("_")
201
+ }
202
+ snapshot |= {
203
+ f.relative_to(dir_path)
204
+ for f in dir_path.glob("**/*.yaml")
205
+ if not f.name.startswith("_")
206
+ }
207
+
208
+ _index_cache = _build_index(directory)
209
+ _index_cache_dir = dir_path
210
+ _index_cache_file_set = snapshot
211
+ return _index_cache
212
+
213
+
214
+ def _tokenize(text: str) -> set[str]:
215
+ return set(re.findall(r"[a-z0-9]+", text.lower()))
216
+
217
+
218
+ def _extract_file_paths(sql_snippets: list[str]) -> list[str]:
219
+ paths: list[str] = []
220
+ pattern = re.compile(
221
+ r"""read_(?:csv|csv_auto|parquet|json(?:_auto)?)\(\s*['"]([^'"]+)['"]""",
222
+ re.IGNORECASE,
223
+ )
224
+ for snippet in sql_snippets:
225
+ for match in pattern.findall(snippet):
226
+ if match not in paths:
227
+ paths.append(match)
228
+ return paths
229
+
230
+
231
+ def _score_entry(
232
+ entry: dict[str, Any], query_tokens: list[str]
233
+ ) -> tuple[float, list[str]]:
234
+ score = 0.0
235
+ reasons: list[str] = []
236
+
237
+ title_tokens = _tokenize(entry["title"])
238
+ desc_tokens = _tokenize(entry["description"])
239
+ tag_set = set(entry["tags"])
240
+ query_name_tokens = _tokenize(" ".join(entry["query_names"]))
241
+ chart_name_tokens = _tokenize(" ".join(entry["chart_names"]))
242
+ sql_tokens = _tokenize(" ".join(entry["sql_snippets"]))
243
+
244
+ for qt in query_tokens:
245
+ if qt in title_tokens:
246
+ score += 3.0
247
+ if "title_match" not in reasons:
248
+ reasons.append("title_match")
249
+
250
+ if qt in tag_set:
251
+ score += 2.5
252
+ if "tag_match" not in reasons:
253
+ reasons.append("tag_match")
254
+
255
+ if qt in desc_tokens:
256
+ score += 1.5
257
+ if "description_match" not in reasons:
258
+ reasons.append("description_match")
259
+
260
+ if qt in query_name_tokens or qt in chart_name_tokens:
261
+ score += 1.0
262
+ if "metric_overlap" not in reasons:
263
+ reasons.append("metric_overlap")
264
+
265
+ if qt in sql_tokens:
266
+ score += 0.5
267
+ if "sql_match" not in reasons:
268
+ reasons.append("sql_match")
269
+
270
+ return score, reasons
@@ -0,0 +1,141 @@
1
+ """File-based install of wheel workflow skills into agent skill directories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from collections.abc import Set
7
+ from pathlib import Path
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+ from dataface.agent_api.skill_render import render_skill_body
12
+ from dataface.agent_api.skills import Skill, all_skill_names, list_skills
13
+
14
+ # Pattern skills are not file-installed; tombstone retired dirs on re-run.
15
+ RETIRED_SKILL_NAMES: tuple[str, ...] = (
16
+ "before-after-comparison",
17
+ "drill-down-link",
18
+ "faceted-small-multiples",
19
+ "filter-bar-with-variables",
20
+ "kpi-row",
21
+ "single-metric-bignum",
22
+ "table-heavy-ops-dashboard",
23
+ "time-series-trend",
24
+ "top-n-with-detail",
25
+ "two-by-two-grid-overview",
26
+ )
27
+
28
+ _LEGACY_SKILL_ROOTS: tuple[Path, ...] = (
29
+ Path(".cursor/skills"),
30
+ Path(".codex/skills"),
31
+ )
32
+
33
+ _SKILL_TARGET_DIRS: dict[str, Path] = {
34
+ "agents": Path(".agents/skills"),
35
+ "codex": Path(".agents/skills"),
36
+ "claude": Path(".claude/skills"),
37
+ }
38
+
39
+ SKILL_INSTALL_TARGETS: frozenset[str] = frozenset(_SKILL_TARGET_DIRS)
40
+
41
+
42
+ class InstallSkillsResult(BaseModel):
43
+ installed: list[str] = Field(default_factory=list)
44
+ retired_removed: list[str] = Field(default_factory=list)
45
+ skipped_existing: list[str] = Field(default_factory=list)
46
+ legacy_dirs_detected: list[Path] = Field(default_factory=list)
47
+
48
+
49
+ def skills_for_file_install() -> list[Skill]:
50
+ """Workflow skills exposed on the CLI surface."""
51
+ return sorted(
52
+ (s for s in list_skills(surface="cli").skills if s.kind == "workflow"),
53
+ key=lambda s: s.name,
54
+ )
55
+
56
+
57
+ def target_dir_for(target: str, *, project_root: Path) -> Path:
58
+ rel = _SKILL_TARGET_DIRS[target]
59
+ return project_root / rel
60
+
61
+
62
+ def detect_skill_targets(project_root: Path) -> list[str]:
63
+ """Return install target keys detected under ``project_root``."""
64
+ targets: list[str] = []
65
+ if (project_root / ".cursor").is_dir() or (project_root / "AGENTS.md").is_file():
66
+ targets.append("agents")
67
+ if (project_root / "CLAUDE.md").is_file():
68
+ targets.append("claude")
69
+ return targets
70
+
71
+
72
+ def detect_legacy_skill_dirs(
73
+ project_root: Path, wheel_skill_names: Set[str]
74
+ ) -> list[Path]:
75
+ """Legacy triplicate dirs that still contain a wheel skill name."""
76
+ found: list[Path] = []
77
+ for rel_root in _LEGACY_SKILL_ROOTS:
78
+ root = project_root / rel_root
79
+ if not root.is_dir():
80
+ continue
81
+ for child in root.iterdir():
82
+ if child.is_dir() and child.name in wheel_skill_names:
83
+ found.append(child)
84
+ return found
85
+
86
+
87
+ def _rendered_skill_md(skill: Skill) -> str:
88
+ raw = (skill.directory / "SKILL.md").read_text(encoding="utf-8")
89
+ if not raw.startswith("---"):
90
+ raise ValueError(f"{skill.directory / 'SKILL.md'}: missing frontmatter")
91
+ parts = raw.split("---", 2)
92
+ if len(parts) < 3:
93
+ raise ValueError(f"{skill.directory / 'SKILL.md'}: malformed frontmatter")
94
+ rendered_body = render_skill_body(skill.body, surface="cli")
95
+ return f"---{parts[1]}---\n{rendered_body}"
96
+
97
+
98
+ def install_skills(
99
+ *,
100
+ target_dir: Path,
101
+ project_root: Path,
102
+ force: bool = False,
103
+ check: bool = False,
104
+ ) -> InstallSkillsResult:
105
+ """Install CLI-rendered workflow skills into ``target_dir``."""
106
+ wheel_names = all_skill_names()
107
+ legacy = detect_legacy_skill_dirs(project_root, wheel_names)
108
+ retired_removed: list[str] = []
109
+ installed: list[str] = []
110
+ skipped_existing: list[str] = []
111
+
112
+ if not check:
113
+ target_dir.mkdir(parents=True, exist_ok=True)
114
+ for name in RETIRED_SKILL_NAMES:
115
+ retired_path = target_dir / name
116
+ if retired_path.is_dir():
117
+ shutil.rmtree(retired_path)
118
+ retired_removed.append(name)
119
+
120
+ for skill in skills_for_file_install():
121
+ dest_dir = target_dir / skill.name
122
+ dest_md = dest_dir / "SKILL.md"
123
+ content = _rendered_skill_md(skill)
124
+ if check:
125
+ installed.append(skill.name)
126
+ continue
127
+ if dest_md.is_file() and not force:
128
+ skipped_existing.append(skill.name)
129
+ continue
130
+ if dest_dir.is_dir():
131
+ shutil.rmtree(dest_dir)
132
+ dest_dir.mkdir(parents=True, exist_ok=True)
133
+ dest_md.write_text(content, encoding="utf-8")
134
+ installed.append(skill.name)
135
+
136
+ return InstallSkillsResult(
137
+ installed=installed,
138
+ retired_removed=retired_removed,
139
+ skipped_existing=skipped_existing,
140
+ legacy_dirs_detected=legacy,
141
+ )