dataface 0.2.1.dev516__py3-none-any.whl → 0.2.1.dev647__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 (256) hide show
  1. dataface/DATAFACE_SYNTAX.md +21 -26
  2. dataface/_install_hint.py +51 -11
  3. dataface/agent_api/__init__.py +9 -3
  4. dataface/agent_api/_paths.py +82 -5
  5. dataface/agent_api/cache.py +55 -10
  6. dataface/agent_api/dashboards.py +23 -67
  7. dataface/agent_api/describe.py +33 -34
  8. dataface/agent_api/describe_query.py +2 -2
  9. dataface/agent_api/diagnostics.py +217 -0
  10. dataface/agent_api/docs/_loader.py +37 -38
  11. dataface/agent_api/docs/yaml-reference.md +49 -27
  12. dataface/agent_api/files.py +1 -1
  13. dataface/agent_api/mcp_install.py +142 -41
  14. dataface/agent_api/project_session.py +7 -14
  15. dataface/agent_api/query.py +20 -18
  16. dataface/agent_api/schema_hints.py +11 -10
  17. dataface/agent_api/search.py +2 -4
  18. dataface/agent_api/surface_aliases.yaml +2 -2
  19. dataface/agent_api/validate.py +49 -48
  20. dataface/ai/agent.py +16 -17
  21. dataface/ai/llm.py +0 -17
  22. dataface/ai/mcp/server.py +30 -23
  23. dataface/ai/prompts/sql-guidance.md +1 -1
  24. dataface/ai/skills/dashboard-build/SKILL.md +1 -1
  25. dataface/ai/skills/dashboard-replicate/SKILL.md +38 -6
  26. dataface/ai/skills/dataface-troubleshooting/SKILL.md +2 -3
  27. dataface/ai/skills/drill-down-link/SKILL.md +11 -5
  28. dataface/ai/tool_schemas.py +8 -14
  29. dataface/ai/tools/__init__.py +41 -16
  30. dataface/cli/_error_format.py +41 -53
  31. dataface/cli/commands/chat.py +1 -1
  32. dataface/cli/commands/describe.py +2 -2
  33. dataface/cli/commands/docs.py +48 -21
  34. dataface/cli/commands/mcp_init.py +13 -29
  35. dataface/cli/commands/render.py +80 -63
  36. dataface/cli/commands/search.py +2 -2
  37. dataface/cli/commands/serve.py +9 -12
  38. dataface/cli/commands/validate.py +3 -3
  39. dataface/cli/filesystem_project.py +40 -1
  40. dataface/cli/main.py +33 -17
  41. dataface/core/compile/__init__.py +1 -1
  42. dataface/core/compile/authoring_warnings.py +46 -58
  43. dataface/core/compile/compiler.py +620 -164
  44. dataface/core/compile/config.py +93 -2
  45. dataface/core/compile/errors.py +16 -17
  46. dataface/core/compile/introspection.py +67 -55
  47. dataface/core/compile/merge.py +4 -2
  48. dataface/core/compile/meta.py +12 -0
  49. dataface/core/compile/models/cache.py +390 -0
  50. dataface/core/compile/models/chart/authored/__init__.py +16 -0
  51. dataface/core/compile/models/chart/authored/_base.py +8 -0
  52. dataface/core/compile/models/chart/resolved/_base.py +8 -0
  53. dataface/core/compile/models/chart/resolved/callout.py +12 -0
  54. dataface/core/compile/models/config.py +52 -0
  55. dataface/core/compile/models/face/authored.py +20 -0
  56. dataface/core/compile/models/factories.py +4 -2
  57. dataface/core/compile/models/primitives.py +38 -8
  58. dataface/core/compile/models/query/authored.py +22 -6
  59. dataface/core/compile/models/query/normalized.py +24 -8
  60. dataface/core/compile/models/refs.py +20 -9
  61. dataface/core/compile/models/source.py +35 -3
  62. dataface/core/compile/models/style/resolved/_base.py +25 -3
  63. dataface/core/compile/models/style/resolved/_cascade.py +46 -4
  64. dataface/core/compile/models/style/theme/__init__.py +4 -0
  65. dataface/core/compile/models/style/theme/axis.py +29 -2
  66. dataface/core/compile/models/style/theme/board.py +8 -1
  67. dataface/core/compile/models/style/theme/legend.py +19 -5
  68. dataface/core/compile/normalize_charts.py +55 -11
  69. dataface/core/compile/normalize_layout.py +71 -7
  70. dataface/core/compile/normalize_queries.py +92 -26
  71. dataface/core/compile/normalize_variables.py +92 -25
  72. dataface/core/compile/normalizer.py +99 -15
  73. dataface/core/compile/palette.py +42 -17
  74. dataface/core/compile/parser.py +13 -20
  75. dataface/core/compile/resolve/_resolve.py +432 -121
  76. dataface/core/compile/schema_renderers/completion_catalog.py +0 -2
  77. dataface/core/compile/schema_renderers/highlight_manifest.py +11 -4
  78. dataface/core/compile/schema_renderers/json_schema.py +120 -12
  79. dataface/core/compile/schema_renderers/prompt.py +38 -4
  80. dataface/core/compile/schema_renderers/vscode_schema.py +42 -22
  81. dataface/core/compile/schema_renderers/yaml_schema.py +26 -6
  82. dataface/core/compile/source_map.py +211 -0
  83. dataface/core/compile/sql_guard.py +1 -1
  84. dataface/core/compile/style_cascade.py +88 -43
  85. dataface/core/compile/tick_values.py +15 -0
  86. dataface/core/compile/typography.py +13 -10
  87. dataface/core/compile/validator.py +3 -3
  88. dataface/core/compile/variables.py +1 -1
  89. dataface/core/compile/yaml_error_formatter.py +40 -123
  90. dataface/core/dashboard.py +168 -187
  91. dataface/core/defaults/default_config.yml +26 -0
  92. dataface/core/defaults/themes/_base.yaml +7 -1
  93. dataface/core/defaults/themes/editorial.yaml +13 -0
  94. dataface/core/defaults/themes/neon.yaml +6 -0
  95. dataface/core/defaults/themes/vivid.yaml +7 -0
  96. dataface/core/diagnostics/__init__.py +227 -0
  97. dataface/core/diagnostics/base.py +91 -0
  98. dataface/core/{errors → diagnostics}/chart_data.py +3 -3
  99. dataface/core/diagnostics/codes_compile.py +601 -0
  100. dataface/core/diagnostics/codes_execute.py +238 -0
  101. dataface/core/diagnostics/codes_query.py +98 -0
  102. dataface/core/diagnostics/codes_render.py +727 -0
  103. dataface/core/diagnostics/codes_serve.py +44 -0
  104. dataface/core/diagnostics/codes_unknown.py +20 -0
  105. dataface/core/diagnostics/diagnostic.py +214 -0
  106. dataface/core/{errors → diagnostics}/execution.py +16 -13
  107. dataface/core/diagnostics/from_query_diagnostic.py +54 -0
  108. dataface/core/diagnostics/hints.py +47 -0
  109. dataface/core/diagnostics/registry.py +152 -0
  110. dataface/core/diagnostics/render_reference.py +128 -0
  111. dataface/core/diagnostics/suppression.py +85 -0
  112. dataface/core/dialects/athena.py +3 -42
  113. dataface/core/dialects/base.py +0 -54
  114. dataface/core/dialects/bigquery.py +1 -2
  115. dataface/core/dialects/databricks.py +0 -31
  116. dataface/core/dialects/duckdb.py +0 -1
  117. dataface/core/dialects/mysql.py +2 -20
  118. dataface/core/dialects/postgres.py +0 -1
  119. dataface/core/dialects/snowflake.py +2 -17
  120. dataface/core/dialects/sqlite.py +0 -1
  121. dataface/core/dialects/sqlserver.py +1 -41
  122. dataface/core/execute/__init__.py +1 -30
  123. dataface/core/execute/_duckdb_cache_base.py +180 -70
  124. dataface/core/execute/adapters/adapter_registry.py +69 -52
  125. dataface/core/execute/adapters/base.py +65 -3
  126. dataface/core/execute/adapters/dbt_adapter.py +19 -10
  127. dataface/core/execute/adapters/duckdb_adapter.py +47 -31
  128. dataface/core/execute/adapters/schema_adapter.py +1 -1
  129. dataface/core/execute/adapters/sql_adapter.py +109 -72
  130. dataface/core/execute/adapters/sqlite_adapter.py +26 -17
  131. dataface/core/execute/cache_backend.py +97 -28
  132. dataface/core/execute/cache_keys.py +15 -3
  133. dataface/core/execute/collect.py +83 -0
  134. dataface/core/execute/executor.py +176 -352
  135. dataface/core/execute/file_source_materializer.py +4 -3
  136. dataface/core/execute/source_resolver.py +58 -31
  137. dataface/core/execute/trivial_local_cache.py +108 -74
  138. dataface/core/inspect/__init__.py +2 -2
  139. dataface/core/inspect/bulk_schema.py +1 -1
  140. dataface/core/inspect/partials/categorical.yml +2 -4
  141. dataface/core/inspect/partials/date.yml +2 -4
  142. dataface/core/inspect/partials/numeric.yml +2 -4
  143. dataface/core/inspect/query_validator.py +63 -28
  144. dataface/core/inspect/renderer.py +11 -70
  145. dataface/core/inspect/resolver.py +17 -13
  146. dataface/core/inspect/templates/categorical_column.yml +1 -8
  147. dataface/core/inspect/templates/charts.yml +1 -8
  148. dataface/core/inspect/templates/date_column.yml +1 -8
  149. dataface/core/inspect/templates/model.yml +1 -8
  150. dataface/core/inspect/templates/numeric_column.yml +1 -8
  151. dataface/core/inspect/templates/quality.yml +1 -5
  152. dataface/core/inspect/templates/string_column.yml +1 -8
  153. dataface/core/project.py +134 -1
  154. dataface/core/registered_views/query_runner.py +19 -7
  155. dataface/core/registered_views/render_pipeline.py +14 -32
  156. dataface/core/render/__init__.py +1 -1
  157. dataface/core/render/board_links.py +7 -7
  158. dataface/core/render/chart/auto_link.py +6 -3
  159. dataface/core/render/chart/callout.py +2 -2
  160. dataface/core/render/chart/data_table_attachment.py +16 -16
  161. dataface/core/render/chart/emitters/__init__.py +2 -2
  162. dataface/core/render/chart/emitters/_cartesian.py +29 -5
  163. dataface/core/render/chart/emitters/_overlay.py +120 -28
  164. dataface/core/render/chart/emitters/area.py +3 -5
  165. dataface/core/render/chart/emitters/bar.py +46 -41
  166. dataface/core/render/chart/emitters/heatmap.py +20 -1
  167. dataface/core/render/chart/emitters/line.py +3 -5
  168. dataface/core/render/chart/emitters/scatter.py +8 -1
  169. dataface/core/render/chart/features/endpoint_labels.py +47 -21
  170. dataface/core/render/chart/features/facet.py +1 -1
  171. dataface/core/render/chart/features/mirror_axis.py +1 -1
  172. dataface/core/render/chart/features/value_labels.py +3 -3
  173. dataface/core/render/chart/geo_tooltip.py +3 -3
  174. dataface/core/render/chart/kpi.py +3 -3
  175. dataface/core/render/chart/rendering.py +35 -27
  176. dataface/core/render/chart/step_band.py +1 -1
  177. dataface/core/render/chart/table.py +367 -160
  178. dataface/core/render/chart/time_unit_detect.py +6 -4
  179. dataface/core/render/chart/type_inference.py +14 -9
  180. dataface/core/render/chart/validation.py +3 -3
  181. dataface/core/render/chart/vega_lite.py +5 -5
  182. dataface/core/render/chart/vl_field_maps.py +49 -60
  183. dataface/core/render/converters/chart.py +5 -3
  184. dataface/core/render/converters/html.py +7 -3
  185. dataface/core/render/dir_context.py +80 -143
  186. dataface/core/render/errors.py +6 -20
  187. dataface/core/render/face_to_dict.py +18 -15
  188. dataface/core/render/faces.py +18 -4
  189. dataface/core/render/format_utils.py +2 -2
  190. dataface/core/render/json_format.py +2 -2
  191. dataface/core/render/layout_sizing.py +47 -18
  192. dataface/core/render/layouts.py +5 -5
  193. dataface/core/render/nav.py +4 -3
  194. dataface/core/render/render_result.py +5 -6
  195. dataface/core/render/renderer.py +28 -29
  196. dataface/core/render/stable_domain.py +4 -4
  197. dataface/core/render/templates/page.css +7 -2
  198. dataface/core/render/templates/page.html +3 -2
  199. dataface/core/render/templates/scripts/chart_interactivity.js +18 -8
  200. dataface/core/render/terminal_charts.py +1 -1
  201. dataface/core/render/text_format.py +2 -2
  202. dataface/core/render/utils.py +20 -14
  203. dataface/core/render/variable_controls.py +1 -1
  204. dataface/core/render/warnings/__init__.py +2 -2
  205. dataface/core/render/warnings/bar_band_width_too_narrow.py +15 -25
  206. dataface/core/render/warnings/base.py +3 -3
  207. dataface/core/render/warnings/layered_chart_shared_y_axis_scale_mismatch.py +15 -22
  208. dataface/core/render/warnings/likely_currency_or_percent_missing_formatter.py +16 -24
  209. dataface/core/render/warnings/palette_unsupported.py +43 -0
  210. dataface/core/render/warnings/pie_dominant_segment.py +11 -17
  211. dataface/core/render/warnings/pie_too_many_segments.py +13 -17
  212. dataface/core/render/warnings/point_map_out_of_projection.py +13 -19
  213. dataface/core/render/warnings/query_returned_zero_rows.py +12 -15
  214. dataface/core/render/warnings/redundant_encoding.py +11 -20
  215. dataface/core/render/warnings/registry.py +12 -5
  216. dataface/core/render/warnings/table_columns_overflow.py +12 -24
  217. dataface/core/render/warnings/temporal_single_point.py +11 -21
  218. dataface/core/render/warnings/too_many_color_categories.py +14 -18
  219. dataface/core/render/warnings/too_many_x_categories.py +14 -18
  220. dataface/core/render/warnings/value_labels_crowd_width.py +12 -25
  221. dataface/core/render/warnings/y_encoding_mostly_null.py +14 -23
  222. dataface/core/render/yaml_format.py +2 -2
  223. dataface/core/serve/server.py +125 -135
  224. dataface/core/serve/templates/error.html.j2 +7 -72
  225. dataface/core/validate.py +1 -1
  226. dataface/data/highlighting/face/v1.json +2 -12
  227. dataface/integrations/markdown.py +13 -5
  228. {dataface-0.2.1.dev516.dist-info → dataface-0.2.1.dev647.dist-info}/METADATA +3 -2
  229. {dataface-0.2.1.dev516.dist-info → dataface-0.2.1.dev647.dist-info}/RECORD +232 -237
  230. dataface/agent_api/docs/warnings.py +0 -171
  231. dataface/ai/memories.py +0 -85
  232. dataface/core/errors/__init__.py +0 -91
  233. dataface/core/errors/base.py +0 -57
  234. dataface/core/errors/codes_compile.py +0 -372
  235. dataface/core/errors/codes_execute.py +0 -254
  236. dataface/core/errors/codes_render.py +0 -324
  237. dataface/core/errors/codes_serve.py +0 -35
  238. dataface/core/errors/codes_unknown.py +0 -15
  239. dataface/core/errors/hints.py +0 -87
  240. dataface/core/errors/registry.py +0 -48
  241. dataface/core/errors/render_error_reference.py +0 -42
  242. dataface/core/errors/structured.py +0 -88
  243. dataface/core/execute/batch.py +0 -790
  244. dataface/core/scoped_paths.py +0 -114
  245. dataface/core/warnings/__init__.py +0 -16
  246. dataface/core/warnings/base.py +0 -20
  247. dataface/core/warnings/fanout_risk.py +0 -15
  248. dataface/core/warnings/from_query_diagnostic.py +0 -56
  249. dataface/core/warnings/missing_join_predicate.py +0 -13
  250. dataface/core/warnings/query_parse_error.py +0 -14
  251. dataface/core/warnings/reaggregation.py +0 -14
  252. dataface/core/warnings/suppression.py +0 -46
  253. dataface/core/warnings/unreferenced_chart.py +0 -15
  254. {dataface-0.2.1.dev516.dist-info → dataface-0.2.1.dev647.dist-info}/WHEEL +0 -0
  255. {dataface-0.2.1.dev516.dist-info → dataface-0.2.1.dev647.dist-info}/entry_points.txt +0 -0
  256. {dataface-0.2.1.dev516.dist-info → dataface-0.2.1.dev647.dist-info}/licenses/LICENSE +0 -0
@@ -155,7 +155,7 @@ Reference variables inside queries with bare `{{ region }}` — no `variables.`
155
155
  charts:
156
156
  revenue_trend:
157
157
  query: revenue # Named query reference
158
- type: line # See `dft docs charts` for all 29 chart types
158
+ type: line # See `dft docs charts` for all 16 authorable chart types
159
159
  x: month
160
160
  y: total
161
161
  color: segment
@@ -413,7 +413,7 @@ Common fields (all query types):
413
413
  | `filters` | Post-execution result filters |
414
414
  | `limit` | Maximum rows returned |
415
415
  | `pivot` | Table-rendering cross-tab hint: `{column, value}` |
416
- | `ignore` | Diagnostic codes to suppress (e.g. `["fanout_risk"]`) |
416
+ | `ignore` | Diagnostic codes to suppress (e.g. `["WARN-FANOUT-RISK"]`) |
417
417
 
418
418
  SQL fields: `sql`, `setup_sql`.
419
419
  MetricFlow fields: `metrics`, `dimensions`, `time_grain`.
@@ -531,25 +531,25 @@ charts:
531
531
  # bar/area families also accept style.orientation and style.stack
532
532
  ```
533
533
 
534
- ### Chart types (29 total)
534
+ ### Chart types (16 authorable)
535
535
 
536
536
  Set `type:` to one of:
537
537
 
538
- **Basic** (one mark per chart) `bar`, `line`, `area`, `scatter`, `pie`, `donut`, `kpi`, `table`.
538
+ **Basic** (one mark per chart) -- `bar`, `line`, `area`, `scatter`, `pie`, `donut`, `kpi`, `table`.
539
539
 
540
- **Statistical** `boxplot`, `errorbar`, `errorband`, `histogram`, `heatmap`.
540
+ **Statistical** -- `histogram`, `heatmap`.
541
541
 
542
- **Geographic** `geoshape`, `map`, `point_map`, `bubble_map`.
542
+ **Geographic** -- `geoshape`, `map`, `point_map`, `bubble_map`.
543
543
 
544
- **Marks** (direct Vega-Lite mark passthroughs) — `circle`, `square`, `tick`, `rule`, `trail`, `rect`, `arc`, `image`.
544
+ **Overlays** -- `bar`, `line`, `area`, and `scatter` accept a `layers:` field for mixed-mark or dual-axis charts (see [Combo charts](#combo-charts-barlinearea-with-layers) and [Composition](#composition)).
545
545
 
546
- **Overlays** `bar`, `line`, `area`, and `scatter` accept a `layers:` field for mixed-mark or dual-axis charts (see [Combo charts](#combo-charts-barlinearea-with-layers) and [Composition](#composition)).
546
+ **Sparklines** -- `spark_bar` (compact horizontal bars used in profiler cards).
547
547
 
548
- **Sparklines** `spark_bar` (compact horizontal bars used in profiler cards).
548
+ **Auxiliary** -- `callout` (message card with a `style.tone:` field; `message:` required).
549
549
 
550
- **Auxiliary** `callout` (message card with a `style.tone:` field; `message:` required), `auto` (auto-detect from data internal-only; not surfaced in UI dropdowns).
550
+ Note: `donut` is an internal alias for `pie` -- `donut` is accepted but normalizes to `pie` internally. `auto` is an internal sentinel (not authored). `boxplot`, `errorbar`, and `errorband` are Vega-Lite mark types that are not in the authorable surface.
551
551
 
552
- Aliases that map to underlying marks: `scatter` `circle`, `heatmap` `rect`, `pie` / `donut` `arc`, `histogram` `bar` with binning, `map` `geoshape`.
552
+ Type aliases: `scatter` uses a circle mark, `heatmap` uses rect, `pie`/`donut` use arc, `histogram` uses bar with binning, `map` maps to geoshape.
553
553
 
554
554
  ### Shared chart fields
555
555
 
@@ -709,11 +709,6 @@ color: event_count
709
709
  type: histogram
710
710
  x: amount
711
711
 
712
- # boxplot / errorbar / errorband — statistical primitives
713
- type: boxplot
714
- x: region
715
- y: latency
716
-
717
712
  # geoshape — choropleth via GeoJSON
718
713
  type: geoshape
719
714
  geo_source: us-states
@@ -1115,26 +1110,26 @@ rows:
1115
1110
 
1116
1111
  ## Errors
1117
1112
 
1118
- Dataface errors carry a machine-readable code in the form `DF-<CATEGORY>-<SLUG>`. The error message includes the code and a pointer to docs.
1113
+ Dataface errors carry a machine-readable code in the form `ERR-<SLUG>`. The error message includes the code and a pointer to docs; a separate `domain` field on the error records where it originated (the code string itself has no domain segment).
1119
1114
 
1120
1115
  ```
1121
- DatafaceError [DF-RENDER-KPI-MULTIROW]: KPI chart 'revenue' returned 12 rows but `value` is a column reference.
1116
+ DatafaceError [ERR-KPI-MULTIROW]: KPI chart 'revenue' returned 12 rows but `value` is a column reference.
1122
1117
  Add LIMIT 1 or aggregate down to one row.
1123
1118
  ```
1124
1119
 
1125
- Active code categories (see `dataface/core/errors/codes_*.py` for the authoritative registry):
1120
+ Active domains (see `dataface/core/diagnostics/codes_*.py` for the authoritative registry):
1126
1121
 
1127
- | Prefix | Meaning |
1122
+ | Domain | Meaning |
1128
1123
  |--------|---------|
1129
- | `DF-COMPILE-*` | Compile-time errors (missing fields, unknown references, malformed YAML) |
1130
- | `DF-RENDER-*` | Chart rendering errors (wrong row counts, unknown chart types, format issues) |
1131
- | `DF-EXECUTE-*` | Query execution errors (missing sources, inline-source policy, source typing) |
1132
- | `DF-SERVE-*` | Server startup errors (invalid theme, launch failures) |
1133
- | `DF-UNKNOWN-*` | Fallback codes for legacy string-message errors not yet migrated |
1124
+ | `compile` | Compile-time errors (missing fields, unknown references, malformed YAML) |
1125
+ | `render` | Chart rendering errors (wrong row counts, unknown chart types, format issues) |
1126
+ | `execute` | Query execution errors (missing sources, inline-source policy, source typing) |
1127
+ | `serve` | Server startup errors (invalid theme, launch failures) |
1128
+ | `unknown` | Fallback codes for legacy string-message errors not yet migrated |
1134
1129
 
1135
1130
  The registry is being filled out incrementally — some compile-time and variable-resolution errors still raise as plain `DatafaceError` without a structured code.
1136
1131
 
1137
- Run `dft docs error-reference` for the full generated reference — every registered code, its message template, and its docs pointer, straight from `dataface.core.errors.REGISTRY`. Run `dft validate <face>` to validate a face and see structured error details.
1132
+ Run `dft docs errors` to list every registered code with a one-line summary, straight from `dataface.core.diagnostics.REGISTRY`. Run `dft docs error-reference` for the full reference — message template and docs pointer included. (`dft docs warnings` / `dft docs warning-reference` are the equivalents for `WARN-*` codes.) Run `dft validate <face>` to validate a face and see structured error details.
1138
1133
 
1139
1134
  **See also:** `dft docs queries`, `dft docs charts`, `dft docs variables` (where most errors originate),
1140
1135
  `dft docs all` (whole reference for context).
dataface/_install_hint.py CHANGED
@@ -1,26 +1,66 @@
1
- """Canonical install-command hints surfaced from runtime error messages.
1
+ """Install-command hints surfaced from runtime error messages.
2
2
 
3
3
  Centralized here so every CLI/runtime hint stays in lock-step with
4
- ``dataface/README.md`` and the wheel-shipped MCP-setup skill. The bare
5
- ``pip install`` form is used (not ``uv pip install``) — customers may
6
- not have ``uv`` on PATH, and bare ``pip`` is universally available with
7
- any Python install. ``cli/_extras.py`` separately emits ``uv pip install
8
- --python <path>`` when uv is the active installer (the smoke-install CI
9
- test pins both legs); this helper is only the canonical fallback string.
10
-
11
- If you change the canonical form, also update the TypeScript mirror at
12
- ``apps/ide/vscode-extension/src/utils/install-hint.ts``.
4
+ ``dataface/README.md`` and the wheel-shipped MCP-setup skill.
5
+
6
+ This module only ever runs from an install that already exists (a
7
+ missing extra, a missing optional dependency like ``uvicorn``) so the
8
+ right question is "how did *this* install get here", not "is `uv` on
9
+ PATH". A pip/venv install with `uv` merely available on PATH must keep
10
+ recommending `pip`; recommending `uv tool install` there would create a
11
+ second, parallel installation alongside the one already running.
12
+ ``cli/_extras.py`` separately emits ``uv pip install --python <path>``
13
+ when uv is the active installer for *adding an extra to the current
14
+ interpreter* (a different question); this helper is the canonical
15
+ reinstall hint.
16
+
17
+ The TypeScript mirror at
18
+ ``apps/ide/vscode-extension/src/utils/install-hint.ts`` uses a
19
+ *different* signal (is `uv` on PATH) because it runs before anything is
20
+ installed — there is no existing install to inspect there. If you
21
+ change one file, check whether the other's signal still applies before
22
+ mirroring the change.
13
23
  """
14
24
 
15
25
  from __future__ import annotations
16
26
 
27
+ import os
28
+ import sys
29
+ from pathlib import Path
30
+
31
+
32
+ def _is_uv_tool_install() -> bool:
33
+ """True if this process is running from a `uv tool install` environment.
34
+
35
+ `uv tool install` creates an isolated virtualenv under uv's tool
36
+ directory (`uv tool dir`; e.g. `~/.local/share/uv/tools` on Linux,
37
+ the platform's per-user data dir on macOS/Windows, overridable via
38
+ `UV_TOOL_DIR`) with the tool name as an immediate child directory.
39
+ `sys.prefix` is that tool venv's own directory — it's the value uv
40
+ set up when creating the venv, not a filesystem fact to be
41
+ recomputed. Deliberately NOT `Path(sys.executable).resolve()`: a
42
+ uv-tool venv's `bin/python` is a symlink back into uv's *shared*
43
+ interpreter directory (`uv/python/...`), so resolving it walks
44
+ straight out of `uv/tools/` and defeats this check.
45
+ """
46
+ prefix = Path(sys.prefix)
47
+ tool_dir = os.environ.get("UV_TOOL_DIR")
48
+ if tool_dir and prefix.parent == Path(tool_dir):
49
+ return True
50
+ parts = prefix.parts
51
+ return any(
52
+ a == "uv" and b == "tools" for a, b in zip(parts, parts[1:], strict=False)
53
+ )
54
+
17
55
 
18
56
  def install_hint(extra: str | None = None) -> str:
19
- """Return the canonical ``pip install`` one-liner for Dataface.
57
+ """Return the install one-liner that matches how this Dataface got installed.
20
58
 
21
59
  ``extra`` is an optional optional-dependency group (``"mcp"``,
22
60
  ``"chat"``, ``"playground"``, ``"bigquery"``); when set, the
23
61
  returned command installs Dataface with that extras bracket.
24
62
  """
25
63
  spec = f"dataface[{extra}]" if extra else "dataface"
64
+ if _is_uv_tool_install():
65
+ return f'uv tool install "{spec}"'
26
66
  return f'pip install "{spec}"'
@@ -70,7 +70,7 @@ from dataface.core.dashboard import (
70
70
  RenderedDashboard as RenderedDashboard,
71
71
  render_dashboard as render_dashboard,
72
72
  )
73
- from dataface.core.errors.structured import StructuredError as StructuredError
73
+ from dataface.core.diagnostics import Diagnostic as Diagnostic
74
74
 
75
75
  # Execution extension seams: the protocols a composition root injects, plus the
76
76
  # resolver base impls it subclasses. Re-exported so roots (CLI, MCP, serve,
@@ -85,7 +85,11 @@ from dataface.core.execute.source_resolver import (
85
85
  SourceResolver as SourceResolver,
86
86
  )
87
87
  from dataface.core.fonts import get_inter_font_path as get_inter_font_path
88
- from dataface.core.project import Project as Project
88
+ from dataface.core.project import (
89
+ FaceFile as FaceFile,
90
+ InMemoryFace as InMemoryFace,
91
+ Project as Project,
92
+ )
89
93
  from dataface.core.render.board_links import LinkContext as LinkContext
90
94
  from dataface.core.render.errors import RenderError as RenderError
91
95
 
@@ -94,9 +98,12 @@ __all__ = [
94
98
  "CompileResult",
95
99
  "DescribeFaceArgs",
96
100
  "DescribeFaceResult",
101
+ "Diagnostic",
97
102
  "DocsArgs",
98
103
  "DocsResult",
99
104
  "DocsSearchHit",
105
+ "FaceFile",
106
+ "InMemoryFace",
100
107
  "InitResult",
101
108
  "LinkContext",
102
109
  "Project",
@@ -107,7 +114,6 @@ __all__ = [
107
114
  "RenderedDashboard",
108
115
  "RenderError",
109
116
  "SchemaHints",
110
- "StructuredError",
111
117
  "Topic",
112
118
  "ValidateDashboardArgs",
113
119
  "ValidateResult",
@@ -2,20 +2,29 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import os
5
6
  from dataclasses import dataclass
6
7
  from pathlib import Path
7
8
 
8
9
  # tach-ignore(agent_api->cli: runtime host-type guard in no_project_hint; the local-filesystem check needs a non-cli signal on Project — deferred)
9
10
  from dataface.cli.filesystem_project import FilesystemProject
10
- from dataface.core.project import Project
11
+ from dataface.core.diagnostics import (
12
+ ERR_FILE_NOT_FOUND,
13
+ ERR_INTERNAL,
14
+ Diagnostic,
15
+ )
16
+ from dataface.core.diagnostics.base import DatafaceError
17
+ from dataface.core.project import (
18
+ FACES_SUBDIR,
19
+ FaceFile,
20
+ Project,
21
+ ProjectPath,
22
+ assert_relpath,
23
+ )
11
24
  from dataface.core.project_roots import (
12
25
  find_dft_root as find_dft_root,
13
26
  find_project_root as find_project_root,
14
27
  )
15
- from dataface.core.scoped_paths import (
16
- resolve_face_path as resolve_face_path,
17
- resolve_scoped_path as resolve_scoped_path,
18
- )
19
28
 
20
29
 
21
30
  def resolve_project_dir(project_dir: Path | None) -> Path:
@@ -66,6 +75,74 @@ def no_project_hint(project: Project) -> str:
66
75
  )
67
76
 
68
77
 
78
+ def resolve_face_path(path: Path, project: Project) -> ProjectPath:
79
+ """User/agent-supplied face path -> ProjectPath.
80
+
81
+ Absolute and ``..``-leading paths are explicit locations: they relativize
82
+ through the containment seam (``project.path_for_fspath``, a leading
83
+ ``..`` resolving against cwd for CLI shell-nav feel) and are returned
84
+ as-is — no ``faces/`` retry, even when the exact path doesn't exist. A
85
+ bare relative name (no explicit location) gets the faces-first retry: for
86
+ a relpath that doesn't exist in the project store and has no explicit
87
+ ``faces/`` prefix, retries under ``FACES_SUBDIR``. This lets a
88
+ ``search_dashboards`` ``face_path`` of ``'looker/x.yaml'`` (``faces/``
89
+ prefix stripped at search time) resolve to ``faces/looker/x.yaml``
90
+ without producing a ``faces/faces/...`` double prefix when the caller
91
+ already supplies the full ``faces/looker/x.yaml``.
92
+
93
+ Existence is checked via ``project.exists(...)`` so it works for all
94
+ ``Project`` implementations (FilesystemProject, CloudManagedProject
95
+ git-blob store, etc.) — not just the local filesystem. Paths that exist
96
+ at root resolve from root — non-faces paths like ``models/schema.sql``
97
+ are not rewritten under faces/.
98
+ """
99
+ if path.is_absolute():
100
+ return project.path_for_fspath(path)
101
+ if ".." in path.parts:
102
+ return project.path_for_fspath(Path(os.path.normpath(Path.cwd() / path)))
103
+ relpath = str(path)
104
+ assert_relpath(relpath)
105
+ if project.exists(relpath):
106
+ return project.path(relpath)
107
+ if not relpath.startswith(f"{FACES_SUBDIR}/"):
108
+ candidate = f"{FACES_SUBDIR}/{relpath}"
109
+ if project.exists(candidate):
110
+ return project.path(candidate)
111
+ return project.path(relpath) # caller reports not-found
112
+
113
+
114
+ def resolve_face_or_error(path: Path, project: Project) -> FaceFile | Diagnostic:
115
+ """Faces-first resolve a path to a stored ``FaceFile``, or a structured error.
116
+
117
+ The one agent_api seam turning a user-supplied path into the compiler's
118
+ located input — shared by the CLI render verb and the AI render tool so the
119
+ faces-first retry and the ``ERR-FILE-NOT-FOUND`` envelope never drift
120
+ between surfaces. Returns a ``Diagnostic`` for an escaping path
121
+ (``ERR-INTERNAL``) or a missing face (``ERR-FILE-NOT-FOUND``).
122
+ """
123
+ try:
124
+ resolved = resolve_face_path(path, project)
125
+ except ValueError as exc:
126
+ return DatafaceError.from_code(ERR_INTERNAL, message=str(exc)).to_diagnostic()
127
+ if not resolved.exists():
128
+ return DatafaceError.from_code(
129
+ ERR_FILE_NOT_FOUND,
130
+ path=resolved.relpath,
131
+ hint=no_project_hint(project),
132
+ ).to_diagnostic(file=resolved.relpath)
133
+ if (
134
+ isinstance(project, FilesystemProject)
135
+ and not (project.root / resolved.relpath).is_file()
136
+ ):
137
+ # A directory (or other non-file) resolves + exists on disk but read_face
138
+ # would raise past the structured envelope. This is a filesystem-only
139
+ # concern — a non-filesystem store's `exists()` already gates real faces.
140
+ return DatafaceError.from_code(
141
+ ERR_INTERNAL, message=f"Not a file: {resolved.relpath}"
142
+ ).to_diagnostic(file=resolved.relpath)
143
+ return resolved.read_face()
144
+
145
+
69
146
  @dataclass(frozen=True)
70
147
  class FaceRenderContext:
71
148
  """Path resolution result from a face path + project root.
@@ -1,8 +1,13 @@
1
1
  """Cache constructors at the agent_api boundary."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  from collections.abc import Generator
4
6
  from contextlib import contextmanager
5
7
  from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ from dataface.core.compile.config import resolve_cache_boot
6
11
 
7
12
  # Re-exported: TrivialDuckDBCache is the declared return type of open_cache,
8
13
  # so embedders annotating a held cache reach it here without a core import.
@@ -10,29 +15,69 @@ from dataface.core.execute.trivial_local_cache import (
10
15
  TrivialDuckDBCache as TrivialDuckDBCache,
11
16
  )
12
17
 
13
- __all__ = ["TrivialDuckDBCache", "open_cache", "cache_ctx"]
18
+ if TYPE_CHECKING:
19
+ from dataface.cli.filesystem_project import FilesystemProject
20
+
21
+ __all__ = [
22
+ "TrivialDuckDBCache",
23
+ "open_cache",
24
+ "open_project_cache",
25
+ "project_cache_ctx",
26
+ ]
14
27
 
15
28
 
16
29
  def open_cache(path: Path | None) -> TrivialDuckDBCache:
17
- """Open the query-result cache.
30
+ """Open the query-result cache at an already-resolved path.
18
31
 
19
32
  ``path=None`` opens an in-memory cache — ephemeral, discarded when closed.
20
33
  A path opens (or creates, if absent) a persistent DuckDB file, reused
21
34
  across invocations. Callers own the returned cache's lifecycle (close it).
35
+ Does not consult project config — use ``open_project_cache`` for that.
22
36
  """
23
37
  return TrivialDuckDBCache(db_path=path)
24
38
 
25
39
 
26
- @contextmanager
27
- def cache_ctx(path: Path | None) -> Generator[TrivialDuckDBCache]:
28
- """Open a cache scoped to a single command invocation; closes it on exit.
40
+ def open_project_cache(
41
+ project: FilesystemProject,
42
+ *,
43
+ no_cache: bool = False,
44
+ cache_path: Path | None = None,
45
+ ) -> TrivialDuckDBCache | None:
46
+ """Open the query-result cache honoring project config, with overrides.
47
+
48
+ The caller owns the project instance. Pair this with
49
+ ``ProjectSession.from_project(project, cache=...)`` rather than
50
+ ``ProjectSession.open(project_dir, cache=...)`` — ``open()`` builds its own
51
+ project from the path, so composing it with this function constructs two.
29
52
 
30
- Use for CLI verbs and composition roots that need a cache scoped to
31
- a single command invocation. Long-lived processes (server boot, MCP
32
- server) call ``open_cache()`` directly and own the lifecycle.
53
+ Precedence: ``no_cache=True`` (the CLI's ``--no-cache``) skips the cache
54
+ entirely; ``cache_path`` (``--cache PATH``, or its ``DFT_CACHE_PATH`` env
55
+ backing) opens that persistent file. Otherwise the project's
56
+ ``dataface.yml`` ``cache:`` block picks the location — a configured ``path``
57
+ opens that file (created if absent); no path opens the zero-config
58
+ in-memory default. A project-level ``cache: false`` defaults the *cascade*
59
+ to off without closing the door on a nearer scope's opt-in, so it still
60
+ opens a store; the resolved per-query policy decides what gets written.
61
+
62
+ Returns:
63
+ ``None`` only when ``no_cache=True`` — callers pass this straight
64
+ through as ``ProjectSession(cache=...)``.
33
65
  """
34
- cache = open_cache(path)
66
+ boot = resolve_cache_boot(project, no_cache=no_cache, cache_path=cache_path)
67
+ return open_cache(boot.path) if boot.enabled else None
68
+
69
+
70
+ @contextmanager
71
+ def project_cache_ctx(
72
+ project: FilesystemProject,
73
+ *,
74
+ no_cache: bool = False,
75
+ cache_path: Path | None = None,
76
+ ) -> Generator[TrivialDuckDBCache | None]:
77
+ """Context-managed ``open_project_cache``; closes the cache on exit if opened."""
78
+ cache = open_project_cache(project, no_cache=no_cache, cache_path=cache_path)
35
79
  try:
36
80
  yield cache
37
81
  finally:
38
- cache.close()
82
+ if cache is not None:
83
+ cache.close()
@@ -13,17 +13,16 @@ from typing import Any, Literal
13
13
  import yaml
14
14
  from pydantic import BaseModel, ConfigDict, Field, field_serializer
15
15
 
16
- from dataface.agent_api._paths import no_project_hint, resolve_face_path
16
+ from dataface.agent_api._paths import resolve_face_or_error
17
17
  from dataface.core.compile import Face, compile_file
18
18
  from dataface.core.dashboard import RenderedDashboard as RenderedDashboard
19
- from dataface.core.errors import (
20
- DF_COMPILE_FILE_NOT_FOUND,
21
- DF_UNKNOWN_INTERNAL,
22
- StructuredError,
19
+ from dataface.core.diagnostics import Diagnostic
20
+ from dataface.core.project import (
21
+ FACES_SUBDIR,
22
+ Project,
23
+ ProjectDirectory,
24
+ ProjectPath,
23
25
  )
24
- from dataface.core.errors.base import DatafaceError
25
- from dataface.core.project import FACES_SUBDIR, Project, ProjectPath
26
- from dataface.core.warnings.base import RenderWarning
27
26
 
28
27
  # Top-level keys whose presence classifies a YAML mapping as a dashboard.
29
28
  DASHBOARD_KEYS = frozenset({"queries", "charts", "rows", "cols", "grid", "tabs"})
@@ -64,23 +63,27 @@ class SkippedFile(BaseModel):
64
63
 
65
64
 
66
65
  class ListDashboardsResult(BaseModel):
67
- model_config = ConfigDict(frozen=True)
66
+ model_config = ConfigDict(arbitrary_types_allowed=True, frozen=True)
68
67
 
69
68
  success: bool
70
- errors: list[StructuredError] = []
69
+ errors: list[Diagnostic] = []
71
70
  dashboards: list[DashboardSummary] = []
72
71
  count: int = 0
73
- directory: Path
72
+ directory: ProjectDirectory
74
73
  skipped_files: list[SkippedFile] = []
75
74
 
75
+ @field_serializer("directory")
76
+ def _serialize_directory(self, d: ProjectDirectory) -> str:
77
+ return d.relpath
78
+
76
79
 
77
80
  class CompiledDashboard(BaseModel):
78
81
  model_config = ConfigDict(frozen=True)
79
82
 
80
83
  success: bool
81
84
  dashboard: Face | None = None
82
- errors: list[StructuredError] = []
83
- warnings: list[RenderWarning] = []
85
+ errors: list[Diagnostic] = []
86
+ warnings: list[Diagnostic] = []
84
87
  raw_yaml: str | None = None
85
88
 
86
89
 
@@ -182,7 +185,7 @@ def list_dashboards(
182
185
  success=True,
183
186
  dashboards=dashboards,
184
187
  count=len(dashboards),
185
- directory=project.root,
188
+ directory=project.directory(under),
186
189
  skipped_files=skipped,
187
190
  )
188
191
 
@@ -194,63 +197,16 @@ def get_dashboard(
194
197
  include_raw: bool = False,
195
198
  ) -> CompiledDashboard:
196
199
  """Get the compiled structure of a dashboard."""
197
- try:
198
- file_path = resolve_face_path(path, project)
199
- except ValueError as exc:
200
- return CompiledDashboard(
201
- success=False,
202
- errors=[
203
- DatafaceError.from_code(
204
- DF_UNKNOWN_INTERNAL, message=str(exc)
205
- ).to_structured()
206
- ],
207
- )
208
-
209
- if not file_path.exists():
210
- hint = no_project_hint(project)
211
- return CompiledDashboard(
212
- success=False,
213
- errors=[
214
- DatafaceError.from_code(
215
- DF_COMPILE_FILE_NOT_FOUND,
216
- path=str(file_path),
217
- hint=hint,
218
- ).to_structured(file=str(file_path))
219
- ],
220
- )
221
- if not file_path.is_file():
222
- return CompiledDashboard(
223
- success=False,
224
- errors=[
225
- DatafaceError.from_code(
226
- DF_UNKNOWN_INTERNAL,
227
- message=f"Not a file: {path}",
228
- ).to_structured(file=str(file_path))
229
- ],
230
- )
231
-
232
- face_pf = project.path_for_fspath(file_path)
233
- result = compile_file(face_pf, project=project)
234
-
235
- raw_yaml: str | None = None
236
- if include_raw:
237
- try:
238
- raw_yaml = face_pf.read_text()
239
- except OSError as e:
240
- return CompiledDashboard(
241
- success=False,
242
- errors=[
243
- DatafaceError.from_code(
244
- DF_UNKNOWN_INTERNAL,
245
- message=f"Failed to read file: {e}",
246
- ).to_structured(file=str(file_path))
247
- ],
248
- )
200
+ # One faces-first resolve/exists/is-file seam, shared with the render path.
201
+ face = resolve_face_or_error(path, project)
202
+ if isinstance(face, Diagnostic):
203
+ return CompiledDashboard(success=False, errors=[face])
204
+ result = compile_file(face)
249
205
 
250
206
  return CompiledDashboard(
251
207
  success=result.success,
252
208
  dashboard=result.face if result.success else None,
253
209
  errors=list(result.errors),
254
210
  warnings=result.warnings,
255
- raw_yaml=raw_yaml,
211
+ raw_yaml=face.content if include_raw else None,
256
212
  )