dataface 0.1.6.dev476__py3-none-any.whl → 0.1.6.dev558__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 (78) hide show
  1. dataface/DATAFACE_SYNTAX.md +1 -0
  2. dataface/agent_api/describe.py +1 -1
  3. dataface/agent_api/describe_query.py +2 -2
  4. dataface/agent_api/docs/yaml-reference.md +25 -11
  5. dataface/agent_api/project_session.py +8 -1
  6. dataface/agent_api/render_face.py +12 -5
  7. dataface/agent_api/validate.py +53 -2
  8. dataface/ai/skills/dashboard-build/SKILL.md +2 -0
  9. dataface/ai/skills/dashboard-design/SKILL.md +4 -0
  10. dataface/ai/skills/two-by-two-grid-overview/SKILL.md +4 -3
  11. dataface/cli/commands/serve.py +31 -6
  12. dataface/core/compile/__init__.py +2 -4
  13. dataface/core/compile/compiler.py +75 -40
  14. dataface/core/compile/config.py +154 -224
  15. dataface/core/compile/data_table_attachment.py +47 -40
  16. dataface/core/compile/inherit_resolver.py +1 -1
  17. dataface/core/compile/introspection.py +1 -8
  18. dataface/core/compile/merge.py +767 -0
  19. dataface/core/compile/meta.py +22 -55
  20. dataface/core/compile/models/chart/authored.py +7 -6
  21. dataface/core/compile/models/chart/resolved.py +4 -2
  22. dataface/core/compile/models/config.py +21 -126
  23. dataface/core/compile/models/face/authored.py +132 -68
  24. dataface/core/compile/models/face/patch.py +28 -0
  25. dataface/core/compile/models/factories.py +38 -18
  26. dataface/core/compile/models/markers.py +43 -31
  27. dataface/core/compile/models/primitives.py +6 -6
  28. dataface/core/compile/models/source.py +4 -4
  29. dataface/core/compile/models/style/authored.py +6 -5
  30. dataface/core/compile/models/style/resolved.py +23 -87
  31. dataface/core/compile/models/style/theme.py +87 -53
  32. dataface/core/compile/normalize_charts.py +38 -4
  33. dataface/core/compile/normalize_layout.py +39 -41
  34. dataface/core/compile/normalize_queries.py +6 -6
  35. dataface/core/compile/normalizer.py +43 -52
  36. dataface/core/compile/sizing.py +31 -32
  37. dataface/core/compile/sources.py +3 -2
  38. dataface/core/compile/style_cascade.py +28 -86
  39. dataface/core/compile/typography.py +15 -8
  40. dataface/core/compile/vega_config.py +3 -3
  41. dataface/core/compile/yaml_error_formatter.py +35 -5
  42. dataface/core/dashboard.py +4 -4
  43. dataface/core/defaults/default_config.yml +17 -30
  44. dataface/core/defaults/themes/_base.yaml +10 -1
  45. dataface/core/errors/__init__.py +2 -0
  46. dataface/core/errors/codes_compile.py +27 -0
  47. dataface/core/execute/adapters/__init__.py +2 -0
  48. dataface/core/execute/adapters/adapter_registry.py +68 -24
  49. dataface/core/execute/adapters/base.py +13 -0
  50. dataface/core/execute/adapters/dbt_adapter.py +3 -2
  51. dataface/core/execute/adapters/duckdb_adapter.py +503 -0
  52. dataface/core/execute/adapters/sql_adapter.py +56 -468
  53. dataface/core/execute/source_resolver.py +69 -4
  54. dataface/core/inspect/renderer.py +2 -2
  55. dataface/core/project.py +45 -11
  56. dataface/core/registered_views/render_pipeline.py +1 -1
  57. dataface/core/render/board_links.py +37 -18
  58. dataface/core/render/chart/kpi.py +23 -4
  59. dataface/core/render/chart/pipeline.py +1 -2
  60. dataface/core/render/chart/render_single.py +11 -4
  61. dataface/core/render/chart/rendering.py +4 -8
  62. dataface/core/render/chart/standard_renderer.py +447 -123
  63. dataface/core/render/chart/table.py +44 -12
  64. dataface/core/render/chart/time_unit_detect.py +4 -4
  65. dataface/core/render/chrome_css.py +7 -3
  66. dataface/core/render/face_api.py +8 -5
  67. dataface/core/render/faces.py +31 -31
  68. dataface/core/render/renderer.py +4 -5
  69. dataface/core/resolve_face.py +14 -16
  70. dataface/core/serve/server.py +4 -4
  71. dataface/core/validate.py +4 -4
  72. dataface/integrations/markdown.py +2 -2
  73. {dataface-0.1.6.dev476.dist-info → dataface-0.1.6.dev558.dist-info}/METADATA +1 -1
  74. {dataface-0.1.6.dev476.dist-info → dataface-0.1.6.dev558.dist-info}/RECORD +77 -75
  75. dataface/core/serve/bootstrap.py +0 -69
  76. {dataface-0.1.6.dev476.dist-info → dataface-0.1.6.dev558.dist-info}/WHEEL +0 -0
  77. {dataface-0.1.6.dev476.dist-info → dataface-0.1.6.dev558.dist-info}/entry_points.txt +0 -0
  78. {dataface-0.1.6.dev476.dist-info → dataface-0.1.6.dev558.dist-info}/licenses/LICENSE +0 -0
@@ -1119,6 +1119,7 @@ The registry is being filled out incrementally — some compile-time and variabl
1119
1119
  - **`DF-COMPILE-SOURCE-REQUIRED`** — a SQL query has no `source:` and no default is configured. Set the source on the query (`source: my_db`), at the face level (`source:` or `sources.default`), or project-wide via `sources.default` in `dataface.yml`.
1120
1120
  - **`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.
1121
1121
  - **`DF-COMPILE-EXTRA-FIELD`** — the face YAML contains a field not recognised by the schema. Remove it or check the schema for supported keys.
1122
+ - **`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`.
1122
1123
  - **`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.
1123
1124
 
1124
1125
  ### Render errors
@@ -356,7 +356,7 @@ def _describe_one_path(
356
356
  for pf in project.iter_faces(under=under, recursive=True)
357
357
  if pf.is_yaml
358
358
  and not pf.is_private
359
- and not pf.sibling(INSPECT_TEMPLATE_MANIFEST).exists()
359
+ and not pf.parent.file(INSPECT_TEMPLATE_MANIFEST).exists()
360
360
  )
361
361
  if not faces:
362
362
  return [
@@ -75,7 +75,7 @@ def describe_query(
75
75
  cfg = adapter_registry.resolve_source_config(source)
76
76
 
77
77
  if cfg.get("type") == "duckdb":
78
- # Run DESCRIBE via the registry's existing read-only SqlAdapter so
78
+ # Run DESCRIBE via the registry's existing read-only DuckDBAdapter so
79
79
  # we don't open a writable dbt-duckdb connection that would conflict
80
80
  # with dft serve.
81
81
  cols = _duckdb_describe(sql, source, adapter_registry)
@@ -110,7 +110,7 @@ def describe_query(
110
110
  def _duckdb_describe(
111
111
  sql: str, source: str | None, adapter_registry: AdapterRegistry
112
112
  ) -> list[DescribeQueryColumn]:
113
- """Run DESCRIBE ({sql}) via the registry's read-only SqlAdapter."""
113
+ """Run DESCRIBE ({sql}) via the registry's read-only DuckDBAdapter."""
114
114
  from dataface.core.compile.models.query.normalized import SqlQuery
115
115
 
116
116
  result = adapter_registry.execute(SqlQuery(sql=f"DESCRIBE ({sql})", source=source))
@@ -30,7 +30,7 @@ AuthoredFace (dataface) definition from YAML.
30
30
  | `width` | str \| int | ✓ | Width when nested (e.g., '50%', '400px', or an integer in pixels). |
31
31
  | `height` | str \| int | ✓ | Height when nested (e.g., '300px' or an integer in pixels). |
32
32
  | `visible` | bool \| str \| [SingleRowBoolProbe](#singlerowboolprobe) | ✓ | Controls whether this layout item is rendered. Accepts a bool, variable name, Jinja expression, or {query, column} probe. |
33
- | `theme` | str | ✓ | Theme name (e.g., 'editorial', 'cream', 'stark'). |
33
+ | `extends` | str \| list[str] | ✓ | Face name(s) or relative path(s) this face inherits from, low to high priority. |
34
34
  | `auto_link` | bool | ✓ | When True, table charts with no explicit link: automatically link each row to its canonical /data/<source>/<schema>/<table>/detail/ page. Default off. Explicit link: always wins; set link: ~ to suppress per chart. |
35
35
 
36
36
  <a id="sourcessection"></a>
@@ -1149,6 +1149,7 @@ Authored overlay for ChartsStyle. Registry of all chart-type styles plus shared
1149
1149
  | `color` | str \| [StyleColorConfig](#stylecolorconfig) | ✓ | Chart-local static mark color (CSS string) or gradient scale config; None uses the theme palette. |
1150
1150
  | `title` | [TitleStyle](#titlestyle) | ✓ | Chart-level title style override; None inherits the theme title style. |
1151
1151
  | `dashes` | list[list[int]] | ✓ | Ordered list of Vega-Lite strokeDash arrays for line-family categorical encoding; None disables dash emission. |
1152
+ | `default_width` | float | ✓ | Default chart width in pixels when no explicit width is set. |
1152
1153
  | `default_chart_height` | float | ✓ | Fallback chart height in pixels when aspect-ratio sizing is unavailable. |
1153
1154
  | `default_table_height` | float | ✓ | Placeholder table height in pixels; replaced by data-aware row-count sizing at render time. |
1154
1155
  | `label_usable_ratio` | float | ✓ | Fraction of chart width usable for axis labels (0–1); labels are tilted when full labels exceed this width. |
@@ -1222,15 +1223,19 @@ Authored overlay for PageStyle. Page-level (outer HTML canvas) styling.
1222
1223
 
1223
1224
  <a id="footerstyle"></a>
1224
1225
  ## FooterStyle
1225
- Authored overlay for FooterStyle. Authored visibility toggle for the page footer chrome.
1226
+ Authored overlay for FooterStyle. Page footer chrome: visibility, attribution text, font, and rule.
1226
1227
 
1227
1228
  | Field | Type | Optional | Description |
1228
1229
  |-------|------|:--------:|-------------|
1229
1230
  | `visible` | bool | ✓ | Show the footer attribution line. |
1231
+ | `text` | str | ✓ | Attribution text shown in the footer. |
1232
+ | `font` | [FontStyle](#fontstyle) | ✓ | Footer text font style (size and color required). |
1233
+ | `y_offset` | float | ✓ | Vertical offset from bottom edge in pixels. |
1234
+ | `rule` | [FooterRule](#footerrule) | ✓ | Hairline rule above footer text; null disables the rule. |
1230
1235
 
1231
1236
  <a id="timestampstyle"></a>
1232
1237
  ## TimestampStyle
1233
- Authored overlay for TimestampStyle. Authored timestamp chrome: visibility, placement, strftime format, and font.
1238
+ Authored overlay for TimestampStyle. Authored timestamp chrome: visibility, placement, strftime format, font, and y-offset.
1234
1239
 
1235
1240
  | Field | Type | Optional | Description |
1236
1241
  |-------|------|:--------:|-------------|
@@ -1238,6 +1243,7 @@ Authored overlay for TimestampStyle. Authored timestamp chrome: visibility, plac
1238
1243
  | `position` | enum: "top", "footer" | ✓ | Timestamp row: top page chrome or footer baseline. |
1239
1244
  | `align` | enum: "left", "right" | ✓ | Timestamp horizontal alignment within its row. |
1240
1245
  | `format` | str | ✓ | strftime format string for the render timestamp. |
1246
+ | `y` | float | ✓ | Y-coordinate for top-positioned timestamp in pixels. |
1241
1247
  | `font` | [FontStyle](#fontstyle) | ✓ | Timestamp font style overrides (size, color, weight, ...). |
1242
1248
 
1243
1249
  <a id="spacingvalues"></a>
@@ -1279,7 +1285,7 @@ A data_table row that reads a column's raw per-x value.
1279
1285
  | Field | Type | Optional | Description |
1280
1286
  |-------|------|:--------:|-------------|
1281
1287
  | `source` | str | | Query column the row reads from (per-x raw value). |
1282
- | `format` | str | ✓ | D3-style format string (Vega-Lite format parity). Optional. |
1288
+ | `format` | str \| [FormatConfig](#formatconfig) | ✓ | D3 format string or format config object. Optional; inherits the chart measure format when omitted and source matches chart.y. |
1283
1289
  | `label` | str | ✓ | Left-stub row label. Optional. |
1284
1290
 
1285
1291
  <a id="chartdatatableaggregate"></a>
@@ -1290,7 +1296,7 @@ A data_table row that reads an aggregate of a column grouped by x.
1290
1296
  |-------|------|:--------:|-------------|
1291
1297
  | `aggregate` | enum: "sum", "avg", "min", "max", "median", "count", "count_distinct" | | Aggregate operation applied per x-group. One of: sum, avg, min, max, median, count, count_distinct. Exact names only — no aliases (spec G4). |
1292
1298
  | `source` | str | | Query column being aggregated. Always required alongside `aggregate:` (spec G2). |
1293
- | `format` | str | ✓ | D3-style format string for the aggregated value. |
1299
+ | `format` | str \| [FormatConfig](#formatconfig) | ✓ | D3 format string or format config object for the aggregated value. Optional. |
1294
1300
  | `label` | str | ✓ | Column header label override for this row. |
1295
1301
 
1296
1302
  <a id="chartdatatableperseries"></a>
@@ -1302,11 +1308,11 @@ A data_table entry that expands into one row per color: series.
1302
1308
  | `per_series` | str | | Query column the row reads from (per-x, per-series value). |
1303
1309
  | `by_measure` | bool | ✓ | When True, expand one row reading the named measure field directly (no color: groupby). Required for multi-y charts where each measure is its own y-field rather than a color-encoded series. |
1304
1310
  | `label` | str | ✓ | Row label displayed in the strip's label gutter. When None and by_measure=True, the per_series field name is used. Has no effect when by_measure=False (series name is the label). |
1305
- | `format` | str | ✓ | D3-style format string (Vega-Lite format parity). Optional. |
1311
+ | `format` | str \| [FormatConfig](#formatconfig) | ✓ | D3 format string or format config object. Optional. |
1306
1312
 
1307
1313
  <a id="fontstyle"></a>
1308
1314
  ## FontStyle
1309
- Text appearance. Cascades as a unit (ADR-005: color inside font).
1315
+ Text appearance. Merged as a unit.
1310
1316
 
1311
1317
  | Field | Type | Optional | Description |
1312
1318
  |-------|------|:--------:|-------------|
@@ -1445,7 +1451,6 @@ Authored overlay for DataTableStyle. Attached data_table style. Lives at style.c
1445
1451
  | `divider` | [RuleStyle](#rulestyle) | ✓ | Rule at the boundary between the chart plot and the data strip. For position='bottom': rule sits above the strip (below the axis). For position='top': rule sits below the strip rows (above the plot top). |
1446
1452
  | `row` | [DataTableRowStyle](#datatablerowstyle) | ✓ | Data_table row padding and rule style. |
1447
1453
  | `label` | [DataTableLabelStyle](#datatablelabelstyle) | ✓ | Row label (series name) style. |
1448
- | `number_align` | enum: "left", "right", "decimal" | ✓ | Alignment of numeric values in data_table cells. |
1449
1454
  | `padding_top` | float | ✓ | Padding above the topmost strip row in pixels. For position='bottom': gap between axis labels and the first row. For position='top': space above the topmost row (outer edge of strip). |
1450
1455
  | `padding_bottom` | float | ✓ | Padding below the last strip row in pixels. For position='bottom': space below the last row. For position='top': gap between the last row and the plot top edge. |
1451
1456
  | `label_max_lines` | int | ✓ | Number of x-axis label lines to reserve in the axis gap (only used for position='bottom'; ignored for position='top'). Typically 1 or 2. |
@@ -2049,6 +2054,15 @@ Authored overlay for InputStyle.
2049
2054
  | `widths` | [InputWidths](#inputwidths) | ✓ | Per-input-type default widths. |
2050
2055
  | `range` | [RangeDefaults](#rangedefaults) | ✓ | Range input default min/max/step values. |
2051
2056
 
2057
+ <a id="footerrule"></a>
2058
+ ## FooterRule
2059
+ Authored overlay for FooterRule. Hairline rule above the footer attribution text. None = no rule.
2060
+
2061
+ | Field | Type | Optional | Description |
2062
+ |-------|------|:--------:|-------------|
2063
+ | `color` | str | ✓ | Rule stroke color. |
2064
+ | `stroke_width` | float | ✓ | Rule stroke width in pixels. |
2065
+
2052
2066
  <a id="legendlabelstyle"></a>
2053
2067
  ## LegendLabelStyle
2054
2068
  Authored overlay for LegendLabelStyle.
@@ -2237,7 +2251,7 @@ Authored overlay for LineMarkStyle. Line mark stroke, interpolation, and halo. P
2237
2251
 
2238
2252
  <a id="pointmarkstyle"></a>
2239
2253
  ## PointMarkStyle
2240
- Authored overlay for PointMarkStyle. Point mark style (data-point markers on line/area/scatter charts).
2254
+ Authored overlay for PointMarkStyle. Point mark style (data-point markers on scatter/point/line/area charts).
2241
2255
 
2242
2256
  | Field | Type | Optional | Description |
2243
2257
  |-------|------|:--------:|-------------|
@@ -2248,7 +2262,7 @@ Authored overlay for PointMarkStyle. Point mark style (data-point markers on lin
2248
2262
  | `filled` | bool | ✓ | Whether points are filled; None uses VL default. |
2249
2263
  | `fill` | str | ✓ | Point interior fill color; only applied when filled=false. |
2250
2264
  | `stroke_width` | float | ✓ | Stroke width in pixels for hollow point rings; None uses VL default. |
2251
- | `labels` | [PointLabelsStyle](#pointlabelsstyle) | ✓ | Value label style for point marks. |
2265
+ | `labels` | [PointLabelsStyle](#pointlabelsstyle) | ✓ | Value label style for point marks. On line charts, setting marks.point.labels is an alias for marks.line.labels. |
2252
2266
 
2253
2267
  <a id="rulemarkstyle"></a>
2254
2268
  ## RuleMarkStyle
@@ -2540,7 +2554,7 @@ Authored overlay for BarLabelsStyle. Bar mark value-label config. Extends MarkLa
2540
2554
  | `dx` | int | ✓ | Horizontal pixel offset for value labels; overrides the position default. |
2541
2555
  | `dy` | int | ✓ | Vertical pixel offset for value labels; overrides the position default. |
2542
2556
  | `font` | [FontStyle](#fontstyle) | ✓ | Value label font style (color, size, family, etc.). |
2543
- | `position` | enum: "top", "inside_top", "middle" | ✓ | Label position relative to the bar. 'top' places labels above the bar top (outside). 'inside_top' places labels just inside the top edge. 'middle' centers labels vertically in the bar. |
2557
+ | `position` | enum: "above", "top", "middle", "middle_aligned", "bottom" | ✓ | Label position relative to the bar. 'above' places labels above the bar top (outside). 'top' places labels just inside the top edge. 'middle' centers labels vertically in the bar. 'middle_aligned' centers all labels at a common height (mean of bar heights / 2). 'bottom' places labels just inside the bottom edge. |
2544
2558
 
2545
2559
  <a id="strokestyle"></a>
2546
2560
  ## StrokeStyle
@@ -30,6 +30,7 @@ from dataface.agent_api.validate import (
30
30
  from dataface.core import dashboard as _core_dashboard
31
31
  from dataface.core.compile.config import (
32
32
  ProjectSourcesConfig,
33
+ get_export_config,
33
34
  )
34
35
  from dataface.core.execute.adapters.adapter_registry import (
35
36
  AdapterRegistry,
@@ -43,12 +44,12 @@ from dataface.core.inspect.query_validator import (
43
44
  )
44
45
  from dataface.core.project import Project
45
46
  from dataface.core.project_roots import find_project_root
47
+ from dataface.core.render.board_links import LinkContext
46
48
 
47
49
  if TYPE_CHECKING:
48
50
  from dataface.core.dashboard import RenderedDashboard, RenderFormat
49
51
  from dataface.core.execute.source_resolver import SourceResolver
50
52
  from dataface.core.inspect.query_validator import RelationshipContext
51
- from dataface.core.render.board_links import LinkContext
52
53
 
53
54
  # Probe at module load time — single find_spec call per process.
54
55
  _SUPER_SCHEMA_AVAILABLE: bool = (
@@ -416,6 +417,12 @@ class ProjectSession:
416
417
  link_context: LinkContext | None = None,
417
418
  **render_options: Any,
418
419
  ) -> RenderedDashboard:
420
+ # When the caller does not supply a link_context, derive origin from the
421
+ # project's public_url config so dft render exports carry fully-qualified links.
422
+ if link_context is None:
423
+ public_url = get_export_config(self.project).public_url
424
+ if public_url:
425
+ link_context = LinkContext(runtime="serve", origin=public_url)
419
426
  return _core_dashboard.render_dashboard(
420
427
  path=path,
421
428
  yaml_content=yaml_content,
@@ -8,13 +8,15 @@ from pathlib import Path
8
8
  from typing import Any
9
9
 
10
10
  from dataface.agent_api.project_session import ProjectSession
11
+ from dataface.core.compile.config import get_export_config
11
12
  from dataface.core.compile.markdown import (
12
13
  MARKDOWN_NOT_FACE_MESSAGE,
13
14
  is_markdown_face,
14
15
  markdown_to_yaml,
15
16
  )
16
17
  from dataface.core.execute.cache_backend import QueryResultCache
17
- from dataface.core.project import Project, ProjectFile
18
+ from dataface.core.project import Project, ProjectDirectory
19
+ from dataface.core.render.board_links import LinkContext
18
20
  from dataface.core.render.face_api import compile_and_render
19
21
 
20
22
 
@@ -76,21 +78,26 @@ def render_face(
76
78
  # project-relative handle; nested sub-file refs are unsupported in that
77
79
  # mode (relative reads still resolve via face_dir below).
78
80
  try:
79
- base_file: ProjectFile | None = p.file_for_path(face_file)
81
+ base_dir: ProjectDirectory | None = p.directory_for_path(face_file.parent)
80
82
  except ValueError:
81
- base_file = None
83
+ base_dir = None
84
+ extra = dict(options)
85
+ if "link_context" not in extra:
86
+ public_url = get_export_config(p).public_url
87
+ if public_url:
88
+ extra["link_context"] = LinkContext(runtime="serve", origin=public_url)
82
89
  return compile_and_render(
83
90
  yaml_content,
84
91
  p,
85
92
  adapter_registry=project_session.adapter_registry,
86
93
  cache=cache,
87
94
  warnings_ignore=warnings_ignore,
88
- base_file=base_file,
95
+ base_dir=base_dir,
89
96
  face_dir=face_file.parent,
90
97
  format=format,
91
98
  variables=variables,
92
99
  use_cache=use_cache,
93
- **options,
100
+ **extra,
94
101
  )
95
102
  finally:
96
103
  project_session.close()
@@ -15,13 +15,20 @@ from dataface.agent_api._paths import (
15
15
  resolve_scoped_path,
16
16
  )
17
17
  from dataface.core.compile.errors import DatafaceError
18
- from dataface.core.errors import DF_UNKNOWN_INTERNAL, StructuredError
18
+ from dataface.core.errors import (
19
+ DF_COMPILE_META_SCHEMA,
20
+ DF_UNKNOWN_INTERNAL,
21
+ StructuredError,
22
+ )
19
23
  from dataface.core.project import Project
20
24
  from dataface.core.render.warnings.base import RenderWarning
21
25
 
22
26
  if TYPE_CHECKING:
23
27
  from dataface.agent_api.project_session import ProjectSession
24
28
 
29
+ # Filenames treated as cascade fragments (FacePatch), not standalone compilable faces.
30
+ _META_FILENAMES: frozenset[str] = frozenset({"meta.yaml", "meta.yml"})
31
+
25
32
 
26
33
  class ValidateDashboardArgs(BaseModel):
27
34
  """Validate a face YAML file without executing queries.
@@ -116,7 +123,8 @@ def _validate_one_path(
116
123
  for pf in project.iter_faces(under=under, recursive=True)
117
124
  if pf.is_yaml
118
125
  and not pf.is_private
119
- and not pf.sibling(INSPECT_TEMPLATE_MANIFEST).exists()
126
+ and Path(pf.relpath).name not in _META_FILENAMES
127
+ and not pf.parent.file(INSPECT_TEMPLATE_MANIFEST).exists()
120
128
  )
121
129
  if not faces:
122
130
  return [
@@ -134,6 +142,46 @@ def _validate_one_path(
134
142
  return [validate(f, project=project) for f in faces]
135
143
 
136
144
 
145
+ def _validate_meta_file(path: Path) -> ValidateResult:
146
+ """Validate a meta.yaml as a FacePatch fragment — no layout required."""
147
+ from pydantic import ValidationError as PydanticValidationError
148
+
149
+ from dataface.core.compile.errors import CompilationError
150
+ from dataface.core.compile.meta import load_meta_file
151
+ from dataface.core.compile.models.face.patch import FacePatch
152
+
153
+ try:
154
+ meta_data, _ = load_meta_file(path)
155
+ except CompilationError as exc:
156
+ return ValidateResult(
157
+ success=False,
158
+ path=path,
159
+ errors=[
160
+ DatafaceError.from_code(
161
+ DF_COMPILE_META_SCHEMA, message=str(exc)
162
+ ).to_structured(file=str(path))
163
+ ],
164
+ )
165
+
166
+ try:
167
+ FacePatch.model_validate(meta_data)
168
+ except PydanticValidationError as exc:
169
+ errors = [
170
+ DatafaceError.from_code(
171
+ DF_COMPILE_META_SCHEMA,
172
+ message=(
173
+ f"{' → '.join(str(loc) for loc in e['loc'])}: {e['msg']}"
174
+ if e["loc"]
175
+ else e["msg"]
176
+ ),
177
+ ).to_structured(file=str(path))
178
+ for e in exc.errors()
179
+ ]
180
+ return ValidateResult(success=False, path=path, errors=errors)
181
+
182
+ return ValidateResult(success=True, path=path)
183
+
184
+
137
185
  def validate(path: Path, *, project: Project) -> ValidateResult:
138
186
  """Fast YAML schema + cross-reference validation. No warehouse, no execute."""
139
187
  from dataface.core.compile.compiler import compile_file
@@ -164,6 +212,9 @@ def validate(path: Path, *, project: Project) -> ValidateResult:
164
212
  ],
165
213
  )
166
214
 
215
+ if resolved.name in _META_FILENAMES:
216
+ return _validate_meta_file(resolved)
217
+
167
218
  try:
168
219
  result = compile_file(project.file_for_path(resolved), project=project)
169
220
  except OSError as exc:
@@ -121,6 +121,8 @@ rows:
121
121
  - cols: [by_region, by_product]
122
122
  ```
123
123
 
124
+ Don't put a `title:` on every row. A section heading over already-labeled charts is repetition, not structure — group with proximity instead. Reach for a row `title:` only when the dashboard is becoming more of a narrative — when there's accompanying `text:` prose, or the heading genuinely says something the charts don't (a shared scope like "Last 30 days", a real mode boundary). A title with no text alongside it usually means the title and the sectioning weren't needed — drop it. (This is dashboard guidance — in a **report**, sections are good: narrative `## …` headings in `text:` blocks carry the prose between charts — see `{{ s_skill_design_report }}`.)
125
+
124
126
  ### Step 6 — Save the Final YAML (when saving applies)
125
127
 
126
128
  Skip this step when you are previewing ephemerally — see "Previewing vs. saving a face" above; your host decides the default. On hosts that offer the user a Save control over the rendered preview (the Cloud chat, for example), saving is the user's click — don't write the file yourself. When the host has no such control and saving applies, write the YAML to a **new** face under `faces/` (never silently fold into an existing one) with your normal file-edit mechanism, then:
@@ -107,6 +107,8 @@ Most important information first, following Western reading pattern (top-left
107
107
  - Related metrics grouped by proximity
108
108
  - Consistent styling throughout
109
109
 
110
+ **Section titles — use sparingly.** A `title:` on a `rows:`/`cols:`/`grid` item renders a heading above that group. On a dense, chart-only dashboard it is almost always noise: the KPIs and chart titles inside already say what each group is, so a row header like "Core Metrics", "Trendlines", or "Overview" just repeats them. Group with proximity and whitespace, not labels. Reach for a section title only when the dashboard is becoming more of a narrative — when there's accompanying `text:` prose that introduces the section, or when the heading genuinely says something the charts don't (a scoping qualifier the charts share like "Last 30 days" or "North America", a real mode boundary the layout alone wouldn't signal). A title with no text alongside it is the tell: usually it means neither the title nor the sectioning was needed — delete it and let the charts speak. This is guidance for designing **dashboards** — in a **report**, sections are good: there the narrative is the point, and `## …` headings in `text:` blocks carry the prose that introduces each section.
111
+
110
112
  ### 5. Color & Visual Design
111
113
 
112
114
  - **Gray is default** — muted everything, ONE accent color for emphasis
@@ -170,6 +172,7 @@ Before delivering:
170
172
  | Pie chart with 7 segments | Use a bar chart instead |
171
173
  | Decorative color | Color must encode data or meaning |
172
174
  | Generic titles | Titles should state what the chart answers |
175
+ | Section title over every row | Drop it — labeled charts don't need a heading repeating them. Reserve titles for reports or a real scoping/mode boundary |
173
176
  | Scrolling dashboard | Reduce charts or split dashboards |
174
177
 
175
178
  ## Rationalizations to Resist
@@ -180,3 +183,4 @@ Before delivering:
180
183
  | "Pie chart is fine for 6 categories" | It's not — humans compare angles poorly. Use bar. |
181
184
  | "The legend explains the colors" | Direct labels are always better than legends. |
182
185
  | "More charts = more value" | More charts = more noise. Each must earn its place. |
186
+ | "Section titles organize the dashboard" | They organize a *report*. On a dense dashboard the charts are already labeled — a heading per row is repetition, not structure. |
@@ -37,8 +37,7 @@ default.
37
37
 
38
38
  ```yaml
39
39
  rows:
40
- - title: Overview
41
- cols:
40
+ - cols:
42
41
  - revenue_trend # top-left
43
42
  - cost_trend # top-right
44
43
  - cols:
@@ -46,6 +45,8 @@ rows:
46
45
  - by_product # bottom-right
47
46
  ```
48
47
 
48
+ No section title — the four labeled charts are self-explanatory, and a heading over them just repeats their titles.
49
+
49
50
  Each chart is defined as usual in `charts:`. The 2×2 is purely a layout
50
51
  decision — swap in any chart type per cell.
51
52
 
@@ -57,7 +58,7 @@ See `examples/two-by-two-grid-overview.yml` for the inline-data worked example.
57
58
  |---|---|---|
58
59
  | 2×3 (six cells) | Add a third `cols:` row | Six equally-weighted metrics |
59
60
  | Asymmetric | `width: "60%"` on one cell | Slightly more emphasis on one chart |
60
- | Section title | `title:` on a `rows:` item | Separate the two rows conceptually |
61
+ | Section title | `title:` on a `rows:` item | Rarely needed — only when a heading adds something the charts don't (a shared scope, a mode boundary). Default to omitting it |
61
62
  | Grid layout | `grid: columns: 24` + `col_span: 12` | Finer column control |
62
63
 
63
64
  ## Common pitfalls
@@ -46,21 +46,46 @@ def serve_command(
46
46
  Returns:
47
47
  None (exits with code 0 on success, 1 on errors)
48
48
  """
49
+ from dataface.core.compile.config import list_built_in_themes, load_config
50
+ from dataface.core.errors import DF_SERVE_INVALID_DEFAULT_THEME
49
51
  from dataface.core.project import Project
50
52
  from dataface.core.project_roots import find_dft_root, infer_dialect_from_dbt
51
- from dataface.core.serve.bootstrap import apply_default_theme
52
53
  from dataface.core.serve.port import resolve_port
53
54
  from dataface.core.serve.server import create_server
54
55
 
55
56
  # Resolve the project root: --project-dir if given, else discover from cwd.
56
57
  project_dir = project_dir or find_dft_root() or Path.cwd()
57
58
 
58
- # Resolve default theme (env var > dataface.yml theme: > built-in default).
59
- # Fails fast on invalid values.
59
+ core_project = Project(project_dir)
60
+
61
+ # Validate DFT_DEFAULT_THEME at startup — fail fast before serving any request.
62
+ # The env var is read per-compilation by get_default_theme_name(); validation
63
+ # here ensures the user sees a clear error at startup rather than on first render.
64
+ env_theme = os.environ.get(
65
+ "DFT_DEFAULT_THEME"
66
+ ) # noqa: TID251 — DFT_DEFAULT_THEME knob
67
+ # Empty string is treated as unset (matches get_default_theme_name, which
68
+ # falls back to the shipped default) — only a non-empty unknown name errors.
69
+ if env_theme:
70
+ available = list_built_in_themes()
71
+ if env_theme not in available:
72
+ err = DatafaceError.from_code(
73
+ DF_SERVE_INVALID_DEFAULT_THEME,
74
+ source="DFT_DEFAULT_THEME env var",
75
+ theme=env_theme,
76
+ available=", ".join(available),
77
+ )
78
+ print_structured_errors([err.to_structured()])
79
+ raise typer.Exit(1) from None
80
+
81
+ # Validate dataface.yml wholesale: stray keys (style:, board:, theme:, etc.) are a
82
+ # hard error here at startup rather than silently ignored.
83
+ from pydantic import ValidationError as _PydanticError
84
+
60
85
  try:
61
- apply_default_theme(Project(project_dir))
62
- except DatafaceError as e:
63
- print_structured_errors([e.to_structured()])
86
+ load_config(core_project)
87
+ except _PydanticError as e:
88
+ typer.echo(f"dataface.yml validation error: {e}", err=True)
64
89
  raise typer.Exit(1) from None
65
90
 
66
91
  # Resolve port: --port > DFT_PORT > dataface.yml server.port > hash(project_dir)
@@ -43,10 +43,9 @@ from dataface.core.compile.errors import (
43
43
  )
44
44
  from dataface.core.compile.meta import (
45
45
  MetaLintConfig,
46
- apply_meta_to_face,
47
46
  find_meta_files,
48
47
  load_meta_file,
49
- resolve_meta_chain,
48
+ resolve_meta_lint,
50
49
  )
51
50
  from dataface.core.compile.models.chart.authored import (
52
51
  AreaChart,
@@ -208,10 +207,9 @@ __all__ = [
208
207
  "get_project_warnings_ignore",
209
208
  # Meta configuration
210
209
  "MetaLintConfig",
211
- "apply_meta_to_face",
212
210
  "find_meta_files",
213
211
  "load_meta_file",
214
- "resolve_meta_chain",
212
+ "resolve_meta_lint",
215
213
  # Type guards
216
214
  "is_face",
217
215
  "is_chart",