dataface 0.2.1.dev47__py3-none-any.whl → 0.2.1.dev164__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 (196) hide show
  1. dataface/DATAFACE_SYNTAX.md +15 -39
  2. dataface/__init__.py +2 -2
  3. dataface/agent_api/dashboards.py +10 -7
  4. dataface/agent_api/data_paths.py +5 -137
  5. dataface/agent_api/describe.py +9 -11
  6. dataface/agent_api/docs/_loader.py +28 -1
  7. dataface/agent_api/docs/yaml-reference.md +17 -15
  8. dataface/agent_api/files.py +74 -70
  9. dataface/agent_api/pack.py +2 -2
  10. dataface/agent_api/project_session.py +22 -70
  11. dataface/agent_api/query.py +1 -4
  12. dataface/agent_api/render_face.py +28 -13
  13. dataface/agent_api/surface_aliases.yaml +4 -12
  14. dataface/agent_api/validate.py +22 -15
  15. dataface/ai/__init__.py +1 -6
  16. dataface/ai/agent.py +81 -23
  17. dataface/ai/generate_sql.py +3 -5
  18. dataface/ai/llm.py +69 -4
  19. dataface/ai/mcp/server.py +8 -7
  20. dataface/ai/skills/analyst-runbook/SKILL.md +3 -2
  21. dataface/ai/skills/dashboard-build/SKILL.md +6 -3
  22. dataface/ai/skills/dashboard-pack-scaffolding/SKILL.md +15 -31
  23. dataface/ai/skills/data-exploration/SKILL.md +48 -40
  24. dataface/ai/skills/dataface-mcp-setup/SKILL.md +0 -2
  25. dataface/ai/skills/dataface-troubleshooting/SKILL.md +11 -5
  26. dataface/ai/tool_schemas.py +0 -6
  27. dataface/ai/tools/__init__.py +14 -86
  28. dataface/cli/commands/chat.py +2 -2
  29. dataface/cli/commands/mcp_init.py +1 -1
  30. dataface/cli/commands/search.py +4 -7
  31. dataface/cli/commands/serve.py +2 -2
  32. dataface/cli/main.py +0 -251
  33. dataface/core/__init__.py +2 -2
  34. dataface/core/compile/authoring_warnings.py +85 -18
  35. dataface/core/compile/config.py +23 -56
  36. dataface/core/compile/filter_injection.py +2 -4
  37. dataface/core/compile/labels_env.py +25 -4
  38. dataface/core/compile/markdown.py +3 -1
  39. dataface/core/compile/merge.py +95 -32
  40. dataface/core/compile/meta.py +1 -1
  41. dataface/core/compile/models/chart/authored/__init__.py +5 -6
  42. dataface/core/compile/models/chart/authored/_annotations.py +2 -45
  43. dataface/core/compile/models/chart/authored/_base.py +1 -8
  44. dataface/core/compile/models/chart/authored/_type.py +0 -4
  45. dataface/core/compile/models/chart/authored/kpi.py +2 -2
  46. dataface/core/compile/models/chart/authored/layered.py +5 -4
  47. dataface/core/compile/models/chart/normalized/pie.py +1 -4
  48. dataface/core/compile/models/chart/resolved/pie.py +1 -5
  49. dataface/core/compile/models/source.py +20 -6
  50. dataface/core/compile/models/style/authored/__init__.py +22 -1
  51. dataface/core/compile/models/style/authored/_base.py +30 -14
  52. dataface/core/compile/models/style/authored/table.py +41 -4
  53. dataface/core/compile/models/style/resolved/_base.py +17 -2
  54. dataface/core/compile/models/style/resolved/_cartesian.py +5 -0
  55. dataface/core/compile/models/style/resolved/_cascade.py +26 -2
  56. dataface/core/compile/models/style/resolved/geoshape.py +5 -0
  57. dataface/core/compile/models/style/resolved/kpi.py +5 -0
  58. dataface/core/compile/models/style/resolved/pie.py +5 -0
  59. dataface/core/compile/models/style/resolved/point_map.py +5 -0
  60. dataface/core/compile/models/style/resolved/spark_bar.py +5 -0
  61. dataface/core/compile/models/style/resolved/table.py +5 -0
  62. dataface/core/compile/models/style/theme/__init__.py +4 -0
  63. dataface/core/compile/models/style/theme/axis.py +145 -11
  64. dataface/core/compile/models/style/theme/board.py +6 -0
  65. dataface/core/compile/models/style/theme/marks.py +21 -14
  66. dataface/core/compile/models/style/theme/page.py +7 -0
  67. dataface/core/compile/models/style/theme/variables.py +8 -6
  68. dataface/core/compile/normalize_charts.py +0 -1
  69. dataface/core/compile/normalize_layout.py +2 -1
  70. dataface/core/compile/normalize_queries.py +13 -12
  71. dataface/core/compile/palette.py +65 -43
  72. dataface/core/compile/parameterized.py +2 -4
  73. dataface/core/compile/parser.py +2 -1
  74. dataface/core/compile/resolve/_resolve.py +164 -36
  75. dataface/core/compile/sql_authoring_lint.py +1 -2
  76. dataface/core/{execute → compile}/sql_guard.py +1 -1
  77. dataface/core/compile/style_cascade.py +9 -5
  78. dataface/core/compile/typography.py +48 -0
  79. dataface/core/compile/vega_lite/mapping.py +2 -12
  80. dataface/core/dashboard.py +63 -15
  81. dataface/core/defaults/palettes/diverging/blue-red-dark.yml +22 -0
  82. dataface/core/defaults/palettes/diverging/coolwarm-dark.yml +23 -0
  83. dataface/core/defaults/palettes/diverging/crimson-green-dark.yml +22 -0
  84. dataface/core/defaults/palettes/diverging/orange-teal-dark.yml +23 -0
  85. dataface/core/defaults/palettes/diverging/sunset-dark.yml +22 -0
  86. dataface/core/defaults/palettes/scaffold/dft-creams.yml +5 -2
  87. dataface/core/defaults/palettes/scaffold/dft-grays.yml +6 -3
  88. dataface/core/defaults/palettes/sequential/amber-dark.yml +22 -0
  89. dataface/core/defaults/palettes/sequential/blue-dark.yml +22 -0
  90. dataface/core/defaults/palettes/sequential/brown-dark.yml +22 -0
  91. dataface/core/defaults/palettes/sequential/gray-dark.yml +22 -0
  92. dataface/core/defaults/palettes/sequential/green-dark.yml +22 -0
  93. dataface/core/defaults/palettes/sequential/purple-dark.yml +22 -0
  94. dataface/core/defaults/palettes/sequential/rust-dark.yml +22 -0
  95. dataface/core/defaults/palettes/sequential/teal-dark.yml +22 -0
  96. dataface/core/defaults/themes/_base.yaml +10 -0
  97. dataface/core/defaults/themes/neon.yaml +35 -24
  98. dataface/core/{execute/dialects → dialects}/__init__.py +12 -12
  99. dataface/core/{execute/dialects → dialects}/athena.py +1 -1
  100. dataface/core/{execute/dialects → dialects}/base.py +43 -1
  101. dataface/core/{execute/dialects → dialects}/bigquery.py +19 -1
  102. dataface/core/{execute/dialects → dialects}/databricks.py +1 -1
  103. dataface/core/{execute/dialects → dialects}/duckdb.py +8 -1
  104. dataface/core/{execute/dialects → dialects}/mysql.py +1 -1
  105. dataface/core/{execute/dialects → dialects}/postgres.py +8 -1
  106. dataface/core/{execute/dialects → dialects}/redshift.py +1 -1
  107. dataface/core/{execute/dialects → dialects}/snowflake.py +19 -1
  108. dataface/core/{execute/dialects → dialects}/sqlite.py +18 -1
  109. dataface/core/{execute/dialects → dialects}/sqlserver.py +1 -1
  110. dataface/core/errors/__init__.py +2 -0
  111. dataface/core/errors/codes_compile.py +27 -16
  112. dataface/core/errors/codes_execute.py +18 -18
  113. dataface/core/errors/codes_render.py +40 -15
  114. dataface/core/errors/codes_serve.py +2 -2
  115. dataface/core/errors/codes_unknown.py +1 -1
  116. dataface/core/{execute/errors.py → errors/execution.py} +4 -2
  117. dataface/core/errors/registry.py +4 -0
  118. dataface/core/errors/render_error_reference.py +42 -0
  119. dataface/core/execute/__init__.py +6 -6
  120. dataface/core/execute/adapters/adapter_registry.py +35 -4
  121. dataface/core/execute/adapters/dbt_adapter.py +17 -4
  122. dataface/core/execute/adapters/dbt_utils.py +4 -24
  123. dataface/core/execute/adapters/duckdb_adapter.py +49 -13
  124. dataface/core/execute/adapters/sql_adapter.py +10 -14
  125. dataface/core/execute/adapters/sqlite_adapter.py +7 -7
  126. dataface/core/execute/batch.py +2 -2
  127. dataface/core/execute/executor.py +15 -7
  128. dataface/core/execute/source_resolver.py +1 -1
  129. dataface/core/inspect/bulk_schema.py +96 -0
  130. dataface/core/inspect/cache_factory.py +21 -11
  131. dataface/core/inspect/query_validator.py +6 -34
  132. dataface/core/inspect/renderer.py +7 -3
  133. dataface/core/inspect/resolver.py +14 -8
  134. dataface/core/inspect/sources/dbt.py +2 -2
  135. dataface/core/pack/planner.py +1 -1
  136. dataface/core/project.py +166 -79
  137. dataface/core/registered_views/render_pipeline.py +1 -1
  138. dataface/core/render/board_links.py +39 -0
  139. dataface/core/render/chart/arc_attached_table.py +0 -6
  140. dataface/core/render/chart/callout.py +6 -1
  141. dataface/core/render/chart/data_table_attachment.py +14 -0
  142. dataface/core/render/chart/emitters/_cartesian.py +39 -2
  143. dataface/core/render/chart/emitters/area.py +8 -5
  144. dataface/core/render/chart/emitters/bar.py +5 -2
  145. dataface/core/render/chart/emitters/heatmap.py +2 -1
  146. dataface/core/render/chart/emitters/layered.py +15 -4
  147. dataface/core/render/chart/emitters/line.py +5 -5
  148. dataface/core/render/chart/emitters/pie.py +9 -20
  149. dataface/core/render/chart/emitters/scatter.py +2 -1
  150. dataface/core/render/chart/features/mirror_axis.py +31 -0
  151. dataface/core/render/chart/labels.py +2 -2
  152. dataface/core/render/chart/presentation.py +3 -42
  153. dataface/core/render/chart/rendering.py +4 -8
  154. dataface/core/render/chart/session.py +6 -6
  155. dataface/core/render/chart/spark_bar.py +12 -8
  156. dataface/core/render/chart/spec.py +5 -1
  157. dataface/core/render/chart/table.py +21 -17
  158. dataface/core/render/chart/table_support.py +13 -3
  159. dataface/core/render/chart/time_unit_detect.py +19 -7
  160. dataface/core/render/chart/translate.py +36 -38
  161. dataface/core/render/chart/type_inference.py +22 -0
  162. dataface/core/render/chart/vega_lite.py +14 -8
  163. dataface/core/render/chart/vl_field_maps.py +63 -0
  164. dataface/core/render/converters/__init__.py +3 -4
  165. dataface/core/render/converters/html.py +27 -33
  166. dataface/core/render/face_resolve.py +5 -160
  167. dataface/core/render/face_to_dict.py +1 -1
  168. dataface/core/render/faces.py +77 -91
  169. dataface/core/render/layout_sizing.py +166 -21
  170. dataface/core/render/layouts.py +0 -8
  171. dataface/core/render/renderer.py +5 -15
  172. dataface/core/render/templates/controls/_styles.css +3 -1
  173. dataface/core/render/templates/scripts/chart_interactivity.js +38 -4
  174. dataface/core/render/templates/scripts/variables.js +13 -0
  175. dataface/core/render/templates/svg/styles.css +11 -0
  176. dataface/core/render/templates/variable_controls/container.html +1 -1
  177. dataface/core/render/variable_controls.py +52 -16
  178. dataface/core/serve/alias_index.py +44 -29
  179. dataface/core/serve/server.py +25 -11
  180. dataface/core/utils.py +35 -0
  181. dataface/integrations/markdown.py +10 -9
  182. {dataface-0.2.1.dev47.dist-info → dataface-0.2.1.dev164.dist-info}/METADATA +1 -1
  183. {dataface-0.2.1.dev47.dist-info → dataface-0.2.1.dev164.dist-info}/RECORD +186 -181
  184. dataface/agent_api/schema.py +0 -656
  185. dataface/agent_api/schema_search.py +0 -286
  186. dataface/ai/schema_context.py +0 -164
  187. dataface/cli/commands/schema.py +0 -593
  188. dataface/core/inspect/dbt_schema.py +0 -96
  189. dataface/core/inspect/fanout_risk.py +0 -109
  190. dataface/core/inspect/search.py +0 -461
  191. dataface/core/render/face_api.py +0 -74
  192. dataface/core/schema/__init__.py +0 -0
  193. dataface/core/schema/guidance.py +0 -150
  194. {dataface-0.2.1.dev47.dist-info → dataface-0.2.1.dev164.dist-info}/WHEEL +0 -0
  195. {dataface-0.2.1.dev47.dist-info → dataface-0.2.1.dev164.dist-info}/entry_points.txt +0 -0
  196. {dataface-0.2.1.dev47.dist-info → dataface-0.2.1.dev164.dist-info}/licenses/LICENSE +0 -0
@@ -26,17 +26,17 @@ A successful result means your data source is reachable.
26
26
 
27
27
  ### Step 2 — Browse your schema
28
28
 
29
- Use `dft schema` to explore available tables and columns before writing SQL:
29
+ Explore available tables and columns with metadata SQL before writing queries:
30
30
 
31
31
  ```bash
32
- dft schema # list configured data sources
33
- dft schema mydb # list schemas in that source
34
- dft schema mydb main # list tables in that schema
35
- dft schema mydb main orders # profile the orders table (columns, types, samples)
36
- dft schema mydb main orders id # profile one column (distribution, sample values)
32
+ dft query mydb "SELECT table_schema, table_name FROM INFORMATION_SCHEMA.TABLES"
33
+ dft query mydb "SELECT column_name, data_type FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'orders'"
37
34
  ```
38
35
 
39
- Never invent column names — always verify them with `dft schema` first.
36
+ (DuckDB also supports `DESCRIBE orders`; SQLite uses `sqlite_master` and
37
+ `PRAGMA table_info(orders)`.)
38
+
39
+ Never invent column names — always verify them against the schema first.
40
40
 
41
41
  ### Step 3 — Build one chart
42
42
 
@@ -573,7 +573,7 @@ All chart types accept the channels and style fields below — but each type rej
573
573
  | `theta` | string | Angular field (pie/donut/arc) |
574
574
  | `style.inner_radius` | float 0–1 | Donut hole ratio (hole/outer disk; pie/donut only); `type: donut` sets it to `0.6` automatically |
575
575
  | `total` | object | `{label, format}` — center total for donut |
576
- | `labels` | object | `{template, where}` — per-row Jinja annotations near anchors |
576
+ | `style.marks.slice.labels` | object | `{template, where}` — per-row Jinja annotations near slice callouts |
577
577
  | `x_label` | string | X-axis title override |
578
578
  | `y_label` | string | Y-axis title override |
579
579
  | `message` | string | Static text for `type: callout` |
@@ -648,11 +648,13 @@ theta: revenue
648
648
  color: segment
649
649
  style:
650
650
  inner_radius: 0.6 # 0 = solid pie; >0 = donut (type: donut sets 0.6 by default)
651
+ marks:
652
+ slice:
653
+ labels:
654
+ template: "{{ segment }}\n{{ revenue | format(',.0f') }}"
651
655
  total:
652
656
  label: Total
653
657
  format: integer
654
- labels:
655
- template: "{{ segment }}\n{{ revenue | format(',.0f') }}"
656
658
 
657
659
  # kpi — requires exactly 1 row; value: is a column reference
658
660
  type: kpi
@@ -1136,38 +1138,12 @@ Active code categories (see `dataface/core/errors/codes_*.py` for the authoritat
1136
1138
  | `DF-COMPILE-*` | Compile-time errors (missing fields, unknown references, malformed YAML) |
1137
1139
  | `DF-RENDER-*` | Chart rendering errors (wrong row counts, unknown chart types, format issues) |
1138
1140
  | `DF-EXECUTE-*` | Query execution errors (missing sources, inline-source policy, source typing) |
1141
+ | `DF-SERVE-*` | Server startup errors (invalid theme, launch failures) |
1139
1142
  | `DF-UNKNOWN-*` | Fallback codes for legacy string-message errors not yet migrated |
1140
1143
 
1141
- The registry is being filled out incrementally — some compile-time and variable-resolution errors still raise as plain `DatafaceError` without a structured code. Today's typed codes:
1142
-
1143
- ### Compile errors
1144
-
1145
- - **`DF-COMPILE-SOURCE-REQUIRED`** — a SQL query has no `source:` and no default is configured. Set the source on the query (`source: my_db`) or at the face level (`source: my_db` at the top of the face YAML).
1146
- - **`DF-COMPILE-SOURCE-INLINE-FORBIDDEN`** — a query's `source:` is an inline connection dict (e.g. `{type: postgres, ...}`). Reference a named source instead (`source: my_db`); connection details live only in `dataface.yml`.
1147
- - **`DF-COMPILE-SOURCE-CREDENTIAL-LITERAL`** — a `dataface.yml` source holds a raw secret literal in a known-secret field (`password`, `private_key`, `keyfile_json`, `token`, `secret`). `dataface.yml` is git-committed, so use `{{ env_var('SECRET_NAME') }}` or a `type: dbt_profile` source instead.
1148
- - **`DF-COMPILE-SOURCE-CONFIG-INVALID`** — a `dataface.yml` `sources:` entry fails typed validation for its `type:` (unknown/missing field, wrong shape).
1149
- - **`DF-COMPILE-SOURCE-NOT-FOUND`** — a query's `source:` names a source that is not in the loaded registry. Check for a typo, or declare the source under `sources:` in `dataface.yml`.
1150
- - **`DF-COMPILE-UNKNOWN-QUERY`** — a chart's `query:` references a name that does not appear under `queries:`. Check for a typo or add the missing query.
1151
- - **`DF-COMPILE-EXTRA-FIELD`** — the face YAML contains a field not recognised by the schema. Remove it or check the schema for supported keys.
1152
- - **`DF-COMPILE-WRONG-SHAPE`** — a YAML field that expects a mapping (nested block) received a scalar or sequence instead. The error hint lists the available keys. For example, `axis_x.label: "hi"` must be a mapping: `axis_x:\n label:\n angle: -45`.
1153
- - **`DF-COMPILE-SQL-LITERAL-NEWLINES`** — a SQL field contains a literal `\n` (backslash + n) from single-quoted YAML. Use a YAML block scalar (`sql: |`) to write multiline SQL.
1154
-
1155
- ### Render errors
1156
-
1157
- - **`DF-RENDER-KPI-MULTIROW`** — a KPI chart's query returned more than one row but `value` is a column reference. Add `LIMIT 1` or aggregate down to one row.
1158
- - **`DF-RENDER-UNKNOWN-CHART-TYPE`** — `type:` is not a recognized chart type. See the [Charts](#charts) section for the list.
1159
- - **`DF-RENDER-FORMAT-UNSUPPORTED`** — the render `format` argument is not one of `svg`/`html`/`png`.
1160
- - **`DF-RENDER-NO-LAYOUT`** — the face has no `rows`/`cols`/`grid`/`tabs` and nothing to render.
1161
- - **`DF-RENDER-INPUT-INVALID`** — the render call received structurally invalid input (e.g. both `path` and `yaml_content`).
1162
-
1163
- ### Execute errors
1164
-
1165
- - **`DF-EXECUTE-SOURCE-NOT-FOUND`** / **`-NOT-FOUND-EMPTY`** — the query's `source:` (or default source) is not configured in `dataface.yml`.
1166
- - **`DF-EXECUTE-NO-DEFAULT-SOURCE`** — multiple sources are defined and no `default_source` was set.
1167
- - **`DF-EXECUTE-SOURCE-INLINE-FORBIDDEN`** / **`-CROSS-FILE-FORBIDDEN`** — inline source definitions are restricted by project policy.
1168
- - **`DF-EXECUTE-SOURCE-INVALID-TYPE`** / **`-MISSING-TYPE`** — a source definition has an unknown or missing `type:` field.
1144
+ The registry is being filled out incrementally — some compile-time and variable-resolution errors still raise as plain `DatafaceError` without a structured code.
1169
1145
 
1170
- Run `dft validate <face>` to validate a face and see structured error details.
1146
+ 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.
1171
1147
 
1172
1148
  **See also:** `dft docs queries`, `dft docs charts`, `dft docs variables` (where most errors originate),
1173
1149
  `dft docs all` (whole reference for context).
dataface/__init__.py CHANGED
@@ -22,8 +22,8 @@ Quick Start:
22
22
  ... face = result.face
23
23
  ...
24
24
  ... # Create executor and render
25
- ... from dataface.core.project import Project
26
- ... registry = build_adapter_registry(Project(Path.cwd()))
25
+ ... from dataface.core.project import FilesystemProject
26
+ ... registry = build_adapter_registry(FilesystemProject(Path.cwd()))
27
27
  ... executor = Executor(face, registry, query_registry=result.query_registry)
28
28
  ... svg = render(face, executor, format="svg")
29
29
  """
@@ -13,10 +13,14 @@ 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 resolve_scoped_path
16
+ from dataface.agent_api._paths import no_project_hint, resolve_scoped_path
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 DF_UNKNOWN_INTERNAL, StructuredError
19
+ from dataface.core.errors import (
20
+ DF_COMPILE_FILE_NOT_FOUND,
21
+ DF_UNKNOWN_INTERNAL,
22
+ StructuredError,
23
+ )
20
24
  from dataface.core.errors.base import DatafaceError
21
25
  from dataface.core.project import FACES_SUBDIR, Project, ProjectPath
22
26
  from dataface.core.warnings.base import RenderWarning
@@ -89,9 +93,6 @@ class RenderDashboardArgs(BaseModel):
89
93
  yaml_content: str | None = Field(
90
94
  None, description="YAML content to render directly (use this OR path)"
91
95
  )
92
- project_dir: Path | None = Field(
93
- None, description="Project root for resolving relative paths"
94
- )
95
96
  variables: dict[str, Any] | None = Field(
96
97
  None, description="Variable values to apply to the dashboard"
97
98
  )
@@ -190,12 +191,14 @@ def get_dashboard(
190
191
  )
191
192
 
192
193
  if not file_path.exists():
194
+ hint = no_project_hint(project.root)
193
195
  return CompiledDashboard(
194
196
  success=False,
195
197
  errors=[
196
198
  DatafaceError.from_code(
197
- DF_UNKNOWN_INTERNAL,
198
- message=f"File not found: {path}",
199
+ DF_COMPILE_FILE_NOT_FOUND,
200
+ path=str(file_path),
201
+ hint=hint,
199
202
  ).to_structured(file=str(file_path))
200
203
  ],
201
204
  )
@@ -1,107 +1,16 @@
1
- """Data URL surfacing and alias typo lint.
2
-
3
- Provides:
4
- - DataPathInfo: the agent-facing wire shape for a canonical data URL
5
- (url, resolves_to: "generic" | "authored", face)
6
- - data_paths_for_source/schema/table: compute data URLs and check the
7
- alias index to determine whether a face overrides the generic system view
8
- - data_paths_list: flat list of DataPathInfo (source + schema + table level)
9
- from a SchemaResponse — used for --data-paths CLI flag and agent discovery
10
- - validate_data_aliases: typo lint for /data/ prefixed aliases — checks
11
- source names only (config only, no DB connection required).
1
+ """Data-alias typo lint for /data/ prefixed face aliases.
2
+
3
+ Checks source names only (config only, no DB connection required) — called
4
+ from the validate path, which must stay connection-free.
12
5
  """
13
6
 
14
7
  from __future__ import annotations
15
8
 
16
- from dataclasses import dataclass
17
9
  from pathlib import Path
18
- from typing import TYPE_CHECKING, Any, Literal
19
-
20
- from dataface.core.registered_views.data_urls import (
21
- data_schema_url,
22
- data_source_url,
23
- data_table_url,
24
- )
10
+ from typing import TYPE_CHECKING
25
11
 
26
12
  if TYPE_CHECKING:
27
- from dataface.agent_api.schema import SchemaResponse
28
13
  from dataface.core.project import Project
29
- from dataface.core.serve.alias_index import AliasIndex
30
-
31
-
32
- @dataclass
33
- class DataPathInfo:
34
- """Canonical data URL with resolution status.
35
-
36
- Wire shape (pinned by contract test — any change is a breaking API change):
37
- {"url": "/data/src/schema/table/", "resolves_to": "generic"}
38
- {"url": "...", "resolves_to": "authored", "face": "/sales/"}
39
- """
40
-
41
- url: str
42
- resolves_to: Literal["generic", "authored"]
43
- face: str | None = None
44
-
45
- def to_dict(self) -> dict[str, Any]:
46
- """Serialize to the agent wire shape, omitting face when generic."""
47
- d: dict[str, Any] = {"url": self.url, "resolves_to": self.resolves_to}
48
- if self.face is not None:
49
- d["face"] = self.face
50
- return d
51
-
52
-
53
- def data_paths_for_source(
54
- source: str,
55
- *,
56
- alias_index: AliasIndex,
57
- ) -> DataPathInfo:
58
- """Return the data URL info for a source.
59
-
60
- The canonical URL is /data/<source>/. When a face aliases it the
61
- resolves_to is 'authored' and face is the canonical face URL.
62
- """
63
- url = data_source_url(source)
64
- face = alias_index.lookup(url)
65
- if face is not None:
66
- return DataPathInfo(url=url, resolves_to="authored", face=face)
67
- return DataPathInfo(url=url, resolves_to="generic")
68
-
69
-
70
- def data_paths_for_schema(
71
- source: str,
72
- schema: str,
73
- *,
74
- alias_index: AliasIndex,
75
- ) -> DataPathInfo:
76
- """Return the data URL info for a schema.
77
-
78
- The canonical URL is /data/<source>/<schema>/. When a face aliases it
79
- the resolves_to is 'authored' and face is the canonical face URL.
80
- """
81
- url = data_schema_url(source, schema)
82
- face = alias_index.lookup(url)
83
- if face is not None:
84
- return DataPathInfo(url=url, resolves_to="authored", face=face)
85
- return DataPathInfo(url=url, resolves_to="generic")
86
-
87
-
88
- def data_paths_for_table(
89
- source: str,
90
- schema: str,
91
- table: str,
92
- *,
93
- alias_index: AliasIndex,
94
- ) -> DataPathInfo:
95
- """Return the data URL info for a table.
96
-
97
- The canonical URL is /data/<source>/<schema>/<table>/. When a face
98
- aliases it the resolves_to is 'authored' and face is the canonical face URL.
99
- """
100
- url = data_table_url(source, schema, table)
101
- face = alias_index.lookup(url)
102
- if face is not None:
103
- return DataPathInfo(url=url, resolves_to="authored", face=face)
104
- return DataPathInfo(url=url, resolves_to="generic")
105
14
 
106
15
 
107
16
  def validate_data_aliases(
@@ -157,47 +66,6 @@ def validate_data_aliases(
157
66
  return errors
158
67
 
159
68
 
160
- def data_paths_list(
161
- schema_response: SchemaResponse,
162
- *,
163
- alias_index: AliasIndex,
164
- ) -> list[DataPathInfo]:
165
- """Build a flat list of DataPathInfo from a SchemaResponse.
166
-
167
- Iterates all sources → schemas → tables in the response and returns one
168
- DataPathInfo per source, per schema, and per table, checking the alias
169
- index for author overrides at each level.
170
- """
171
- result: list[DataPathInfo] = []
172
- for source_name, source_data in schema_response.sources.items():
173
- result.append(data_paths_for_source(source_name, alias_index=alias_index))
174
- for schema_name, schema_data in (source_data.get("schemas") or {}).items():
175
- result.append(
176
- data_paths_for_schema(source_name, schema_name, alias_index=alias_index)
177
- )
178
- for table_name in schema_data.get("tables") or {}:
179
- result.append(
180
- data_paths_for_table(
181
- source_name,
182
- schema_name,
183
- table_name,
184
- alias_index=alias_index,
185
- )
186
- )
187
- return result
188
-
189
-
190
- def build_alias_index_for_project(project: Project) -> AliasIndex:
191
- """Build an AliasIndex from a project.
192
-
193
- Intended for CLI commands (dft schema --data-paths) that need alias lookup
194
- without starting a server.
195
- """
196
- from dataface.core.serve.alias_index import AliasIndex
197
-
198
- return AliasIndex.build(project)
199
-
200
-
201
69
  def data_alias_errors_for_file(
202
70
  face_file: Path,
203
71
  source_names: frozenset[str],
@@ -18,7 +18,11 @@ from dataface.core.compile.models.query.normalized import (
18
18
  SqlQuery,
19
19
  ValuesQuery,
20
20
  )
21
- from dataface.core.errors import DF_UNKNOWN_INTERNAL, StructuredError
21
+ from dataface.core.errors import (
22
+ DF_COMPILE_FILE_NOT_FOUND,
23
+ DF_UNKNOWN_INTERNAL,
24
+ StructuredError,
25
+ )
22
26
  from dataface.core.project import Project
23
27
 
24
28
  # Chart-encoding fields surfaced to agents. Covers the stable channel fields
@@ -90,13 +94,6 @@ class DescribeFaceArgs(BaseModel):
90
94
  """Describe the structure of a face file: queries (with type and source), charts (with query bindings and encoding), variables, and layout. Use this for orientation when picking up an unfamiliar dashboard — it reads the YAML and returns a curated narrative summary without executing any queries."""
91
95
 
92
96
  path: Path = Field(description="Path to the dashboard YAML file to describe")
93
- project_dir: Path | None = Field(
94
- default=None,
95
- description=(
96
- "Project root for resolving relative paths. Optional on the wire; "
97
- "the MCP server injects the project root when omitted."
98
- ),
99
- )
100
97
 
101
98
 
102
99
  class DescribeFaceResult(BaseModel):
@@ -208,15 +205,16 @@ def describe_face(path: Path, *, project: Project) -> DescribeFaceResult:
208
205
  ],
209
206
  )
210
207
 
211
- if not resolved.exists():
208
+ if not project.path_for_fspath(resolved).exists():
212
209
  hint = no_project_hint(project.root)
213
210
  return DescribeFaceResult(
214
211
  success=False,
215
212
  path=path,
216
213
  errors=[
217
214
  DatafaceError.from_code(
218
- DF_UNKNOWN_INTERNAL,
219
- message=f"File not found: {resolved}{hint}",
215
+ DF_COMPILE_FILE_NOT_FOUND,
216
+ path=str(resolved),
217
+ hint=hint,
220
218
  ).to_structured()
221
219
  ],
222
220
  )
@@ -5,6 +5,8 @@ are slugified headings. Bare `docs()` returns the topic index (slug + one-line
5
5
  description per H2). `docs(topic="<slug>")` returns one slice.
6
6
  `docs(topic="all")` returns the whole file. `docs(topic="reference")` returns
7
7
  the auto-generated field spec from yaml-reference.md.
8
+ `docs(topic="error-reference")` returns the auto-generated error reference from
9
+ error-reference.md.
8
10
  `docs(search=...)` substring-matches across topics.
9
11
  """
10
12
 
@@ -25,9 +27,15 @@ _REFERENCE_FILE = (
25
27
  importlib.resources.files("dataface.agent_api.docs") / "yaml-reference.md"
26
28
  )
27
29
 
30
+ # Generated error reference ships inside the wheel at dataface/agent_api/docs/error-reference.md.
31
+ _ERROR_REFERENCE_FILE = (
32
+ importlib.resources.files("dataface.agent_api.docs") / "error-reference.md"
33
+ )
34
+
28
35
  _TOPIC_RE = re.compile(r"^[a-z0-9-]+$")
29
36
  _ALL_TOPIC = "all"
30
37
  _REFERENCE_TOPIC = "reference"
38
+ _ERROR_REFERENCE_TOPIC = "error-reference"
31
39
 
32
40
  DocsMode = Literal["index", "topic", "search"]
33
41
 
@@ -68,7 +76,7 @@ class DocsArgs(BaseModel):
68
76
 
69
77
  topic: str | None = Field(
70
78
  None,
71
- description="Topic slug from the `dft docs` topic index (e.g. 'face', 'charts', 'all', 'reference')",
79
+ description="Topic slug from the `dft docs` topic index (e.g. 'face', 'charts', 'all', 'reference', 'error-reference')",
72
80
  )
73
81
  search: str | None = Field(
74
82
  None, description="Full-text query — runs substring search across all topics"
@@ -101,6 +109,7 @@ def docs(
101
109
  - ``docs(topic="face")`` — return content for one H2 section.
102
110
  - ``docs(topic="all")`` — return the full ``DATAFACE_SYNTAX.md`` unsliced.
103
111
  - ``docs(topic="reference")`` — return the auto-generated field spec.
112
+ - ``docs(topic="error-reference")`` — return the auto-generated error reference.
104
113
  - ``docs(search="grid")`` — substring search across H2 sections.
105
114
  """
106
115
  if topic is not None and search is not None:
@@ -145,6 +154,24 @@ def docs(
145
154
  ),
146
155
  )
147
156
 
157
+ if topic == _ERROR_REFERENCE_TOPIC:
158
+ try:
159
+ content = _ERROR_REFERENCE_FILE.read_text(encoding="utf-8")
160
+ except FileNotFoundError:
161
+ return DocsResult(
162
+ success=False,
163
+ mode="topic",
164
+ errors=["error-reference.md is missing from the installed package."],
165
+ )
166
+ return DocsResult(
167
+ mode="topic",
168
+ topic=Topic(
169
+ id=_ERROR_REFERENCE_TOPIC,
170
+ title="Dataface Error Reference (generated)",
171
+ content=content,
172
+ ),
173
+ )
174
+
148
175
  if not _TOPIC_RE.fullmatch(topic):
149
176
  return DocsResult(
150
177
  success=False,
@@ -348,7 +348,6 @@ Authored patch for pie and donut charts.
348
348
  | `subtitle` | str | ✓ | Chart subtitle displayed below the title. |
349
349
  | `color` | str | ✓ | Color data channel: bare column name only. |
350
350
  | `total` | [ChartTotal](#charttotal) | ✓ | Donut center total. |
351
- | `labels` | [ChartLabels](#chartlabels) | ✓ | Per-row text annotations near each data anchor. |
352
351
  | `style` | [PieChartStyle](#piechartstyle) | ✓ | Chart-local style overrides. |
353
352
 
354
353
  <a id="kpichart"></a>
@@ -805,15 +804,6 @@ Donut center total — auto-rendered sum at the center of a donut, with author o
805
804
  | `label` | str | ✓ | Caption text displayed below the center total value. |
806
805
  | `format` | str \| [FormatConfig](#formatconfig) | ✓ | Number format (D3 spec, preset, or FormatConfig). |
807
806
 
808
- <a id="chartlabels"></a>
809
- ## ChartLabels
810
- Per-row text annotations rendered near each data anchor.
811
-
812
- | Field | Type | Optional | Description |
813
- |-------|------|:--------:|-------------|
814
- | `template` | str | | Jinja2 template for the label text. Row fields are available as variables. |
815
- | `where` | str | ✓ | Jinja2 boolean filter expression. Labels only render on rows where this is truthy. |
816
-
817
807
  <a id="piechartstyle"></a>
818
808
  ## PieChartStyle
819
809
  Authored overlay for PieChartStyle. Pie/donut chart style: geometry + total (flat) + marks sub-block.
@@ -1290,6 +1280,7 @@ Authored overlay for FooterStyle. Page footer chrome: visibility, attribution te
1290
1280
  |-------|------|:--------:|-------------|
1291
1281
  | `visible` | bool | ✓ | Show the footer attribution line. |
1292
1282
  | `text` | str | ✓ | Attribution text shown in the footer. |
1283
+ | `link` | str | ✓ | URL the footer brand word 'dataface' links to; null renders plain text. |
1293
1284
  | `font` | [FontStyle](#fontstyle) | ✓ | Footer text font style (size and color required). |
1294
1285
  | `y_offset` | float | ✓ | Vertical offset from bottom edge in pixels. |
1295
1286
  | `rule` | [FooterRule](#footerrule) | ✓ | Hairline rule above footer text; null disables the rule. |
@@ -1451,7 +1442,7 @@ Authored overlay for AxisStyle.
1451
1442
  | `title` | [AxisElementStyle](#axiselementstyle) | ✓ | Axis title style. |
1452
1443
  | `position` | enum: "left", "right", "top", "bottom", "auto" | ✓ | Axis position; auto flips y-axis when endpoint labels are present. |
1453
1444
  | `categorical_orient` | str | ✓ | Orientation for categorical y-axis (e.g. 'left'); None skips this VL property. |
1454
- | `mirror` | bool | ✓ | Draw the y-scale on both left and right edges (wide charts). Only meaningful on axis_y. |
1445
+ | `mirror` | bool \| [AxisMirrorStyle](#axismirrorstyle) | ✓ | Draw the y-scale on both left and right edges (wide charts). true mirrors the primary axis's label verbatim; an object (format/expr) relabels only the mirrored edge — e.g. a percent-of-total right axis next to an absolute-value left axis — while ticks stay aligned to the single shared scale. Only meaningful on axis_y. |
1455
1446
  | `band_position` | float | ✓ | Band position within the step (0–1); None uses Vega-Lite's default. |
1456
1447
  | `offset` | float | ✓ | Pixel offset of the axis from its default position; None means no offset. |
1457
1448
  | `format` | str | ✓ | Tick value format string; None uses auto-format. |
@@ -1480,8 +1471,8 @@ Authored overlay for ScaleStyle. Scale configuration primitive.
1480
1471
  | `quantize_count` | int | ✓ | Number of quantize buckets; None uses Vega-Lite's default. |
1481
1472
  | `use_unaggregated_domain` | bool | ✓ | Use unaggregated domain for scale extent; None uses Vega-Lite's default. |
1482
1473
  | `x_reverse` | bool | ✓ | Reverse the x-axis scale direction; None means no reversal. |
1483
- | `type` | str | ✓ | Scale type override; None lets Vega-Lite infer from field type. |
1484
- | `domain` | list[Any] | ✓ | Explicit scale domain values; None lets Vega-Lite auto-determine from data. |
1474
+ | `type` | enum: "linear", "log", "pow", "sqrt", "symlog", "temporal" | ✓ | Scale type override. 'linear'/'log'/'pow'/'sqrt'/'symlog' pass straight through to Vega-Lite's quantitative scale.type. 'temporal' is a Dataface escape hatch for a cartesian x-axis: it forces a continuous temporal scale instead of the auto-inferred ordinal/nominal bucketed type, which is required before an authored ``domain`` of ISO dates can extend the visible range past the data extent. None lets Dataface/Vega-Lite infer from the field type. |
1475
+ | `domain` | list[Any] | ✓ | Explicit [low, high] scale domain; None lets Vega-Lite auto-determine from data. Must have exactly 2 elements — Dataface only wires a continuous range override through to Vega-Lite, not an explicit category enumeration. When ``type: temporal`` is set, both elements must be ISO-8601 date/datetime strings. |
1485
1476
  | `nice` | bool | ✓ | Round scale domain to nice values; None uses Vega-Lite's default. |
1486
1477
  | `padding` | float | ✓ | Unified scale padding shortcut; dispatches to band, point, or continuous padding per scale type. |
1487
1478
 
@@ -2205,7 +2196,7 @@ Authored overlay for AxisTicksStyle.
2205
2196
  | `color` | str | ✓ | Tick color; None uses Vega-Lite's default. |
2206
2197
  | `size` | float | ✓ | Tick length in pixels; None uses Vega-Lite's default. |
2207
2198
  | `width` | float | ✓ | Tick stroke width in pixels; None uses Vega-Lite's default. |
2208
- | `count` | int | ✓ | Target number of quantitative y-axis ticks. When set, the renderer computes explicit tickValues so VL emits exactly this many ticks — VL's tickCount is advisory and frequently ignored. |
2199
+ | `count` | int | ✓ | Target number of axis ticks. On a quantitative axis the renderer computes explicit tickValues so VL emits exactly this many ticks — VL's tickCount is advisory and frequently ignored there. On a temporal axis_x, count is passed through as VL's axis.tickCount, which VL's time-scale tick logic honours as a target. |
2209
2200
 
2210
2201
  <a id="axiselementstyle"></a>
2211
2202
  ## AxisElementStyle
@@ -2231,6 +2222,15 @@ Authored overlay for AxisElementStyle. Axis label or title: font + padding + VL-
2231
2222
  | `anchor` | enum: "start", "middle", "end" | ✓ | Label anchor position; None uses Vega-Lite's default. |
2232
2223
  | `tilt_increments` | list[float] | ✓ | Descending tilt angles for smart label overlap on discrete x-axes; None disables the picker. |
2233
2224
 
2225
+ <a id="axismirrorstyle"></a>
2226
+ ## AxisMirrorStyle
2227
+ Per-edge label override for the mirrored ``axis_y.mirror`` ghost axis.
2228
+
2229
+ | Field | Type | Optional | Description |
2230
+ |-------|------|:--------:|-------------|
2231
+ | `format` | str | ✓ | Tick value format string for the mirrored edge; None reuses the primary axis's format. |
2232
+ | `expr` | str | ✓ | Custom Vega expression for the mirrored edge's label text; None reuses the primary axis's label expression. |
2233
+
2234
2234
  <a id="scalebandstyle"></a>
2235
2235
  ## ScaleBandStyle
2236
2236
  Authored overlay for ScaleBandStyle. Band mark size constraints (min/max pixel width for band-scale marks).
@@ -2645,7 +2645,9 @@ Authored overlay for SliceLabelsStyle. Pie labels: typography + positioning offs
2645
2645
  | `offset` | float | ✓ | Radial offset of slice labels from the arc in pixels. |
2646
2646
  | `line_height` | float | ✓ | Line height for slice labels in pixels. Reserved vertical space above the disk is ``line_height × &lt;rendered lines&gt;`` per row, so the same value handles 1-line, 2-line, and multi-line templates. |
2647
2647
  | `font` | [FontStyle](#fontstyle) | ✓ | Slice label font style overrides. Unset fields fall back to [`style.charts.font`](#chartsstyle). |
2648
- | `default_template` | [LabelsDefaultTemplate](#labelsdefaulttemplate) | ✓ | Default Jinja templates for per-slice labels when the chart author omits ``labels:`` on a pie/donut. |
2648
+ | `default_template` | [LabelsDefaultTemplate](#labelsdefaulttemplate) | ✓ | Default Jinja templates for per-slice labels when template is not authored. |
2649
+ | `template` | str | ✓ | Jinja2 label template. Overrides default_template when authored. |
2650
+ | `where` | str | ✓ | Jinja2 boolean filter — labels only render on rows where this is truthy. |
2649
2651
 
2650
2652
  <a id="tablerowrolestyle"></a>
2651
2653
  ## TableRowRoleStyle