dataface 0.3.2.dev22__py3-none-any.whl → 0.3.2.dev111__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 (185) hide show
  1. dataface/__init__.py +70 -23
  2. dataface/agent_api/__init__.py +162 -68
  3. dataface/agent_api/_paths.py +9 -3
  4. dataface/agent_api/docs/yaml-reference.md +240 -106
  5. dataface/agent_api/init.py +1 -1
  6. dataface/agent_api/project_session.py +2 -1
  7. dataface/agent_api/serve.py +115 -0
  8. dataface/ai/agent.py +28 -2
  9. dataface/cli/_project.py +100 -1
  10. dataface/cli/commands/board_artifact.py +5 -2
  11. dataface/cli/commands/describe.py +7 -3
  12. dataface/cli/commands/docs.py +12 -3
  13. dataface/cli/commands/init.py +7 -14
  14. dataface/cli/commands/inspect.py +6 -1
  15. dataface/cli/commands/mcp_init.py +23 -43
  16. dataface/cli/commands/migrate.py +8 -3
  17. dataface/cli/commands/query.py +11 -8
  18. dataface/cli/commands/render.py +20 -9
  19. dataface/cli/commands/search.py +8 -2
  20. dataface/cli/commands/serve.py +26 -76
  21. dataface/cli/commands/skills_init.py +2 -19
  22. dataface/cli/commands/validate.py +7 -2
  23. dataface/cli/filesystem_project.py +5 -4
  24. dataface/cli/main.py +2 -19
  25. dataface/core/__init__.py +73 -28
  26. dataface/core/{compile/colors.py → colors.py} +20 -5
  27. dataface/core/compile/compiler.py +97 -34
  28. dataface/core/compile/data_table.py +7 -2
  29. dataface/core/compile/enrich.py +18 -44
  30. dataface/core/compile/face_warnings.py +46 -8
  31. dataface/core/compile/format.py +47 -1
  32. dataface/core/compile/inherit_graph.py +3 -3
  33. dataface/core/compile/inherit_registry.yaml +1 -1
  34. dataface/core/compile/migrations.py +112 -9
  35. dataface/core/compile/models/chart/normalized/kpi.py +0 -4
  36. dataface/core/compile/models/chart/resolved/_base.py +28 -10
  37. dataface/core/compile/models/chart/resolved/callout.py +2 -3
  38. dataface/core/compile/models/chart/resolved/kpi.py +7 -3
  39. dataface/core/compile/models/chart/resolved/table.py +16 -2
  40. dataface/core/compile/models/markers.py +1 -1
  41. dataface/core/compile/models/primitives.py +175 -2
  42. dataface/core/compile/models/source.py +5 -5
  43. dataface/core/compile/models/style/authored/__init__.py +32 -8
  44. dataface/core/compile/models/style/authored/_base.py +126 -69
  45. dataface/core/compile/models/style/authored/table.py +61 -3
  46. dataface/core/compile/models/style/resolved/__init__.py +12 -2
  47. dataface/core/compile/models/style/resolved/_base.py +142 -57
  48. dataface/core/compile/models/style/resolved/_cartesian.py +6 -11
  49. dataface/core/compile/models/style/resolved/_cascade.py +141 -54
  50. dataface/core/compile/models/style/resolved/area.py +1 -1
  51. dataface/core/compile/models/style/resolved/bar.py +1 -1
  52. dataface/core/compile/models/style/resolved/line.py +1 -1
  53. dataface/core/compile/models/style/resolved/scatter.py +3 -2
  54. dataface/core/compile/models/style/theme/__init__.py +36 -14
  55. dataface/core/compile/models/style/theme/_chart_base.py +17 -9
  56. dataface/core/compile/models/style/theme/axis.py +432 -312
  57. dataface/core/compile/models/style/theme/charts.py +10 -6
  58. dataface/core/compile/models/style/theme/kpi.py +2 -1
  59. dataface/core/compile/models/style/theme/page.py +5 -2
  60. dataface/core/compile/models/style/theme/table.py +2 -2
  61. dataface/core/compile/models/vega_lite/config.py +1 -1
  62. dataface/core/compile/normalize_charts.py +6 -2
  63. dataface/core/compile/normalize_queries.py +21 -7
  64. dataface/core/compile/resolve/__init__.py +2 -0
  65. dataface/core/compile/resolve/_axes.py +147 -44
  66. dataface/core/compile/resolve/_channels.py +1 -1
  67. dataface/core/compile/resolve/_dispatch.py +1 -1
  68. dataface/core/compile/resolve/_domain.py +49 -32
  69. dataface/core/compile/resolve/_kwargs.py +39 -12
  70. dataface/core/compile/resolve/_marks.py +8 -3
  71. dataface/core/compile/resolve/_palette.py +7 -3
  72. dataface/core/compile/resolve/_table.py +124 -33
  73. dataface/core/compile/resolve/area.py +48 -18
  74. dataface/core/compile/resolve/bar.py +245 -64
  75. dataface/core/compile/resolve/geo.py +10 -3
  76. dataface/core/compile/resolve/heatmap.py +16 -8
  77. dataface/core/compile/resolve/line.py +35 -12
  78. dataface/core/compile/resolve/pie.py +4 -1
  79. dataface/core/compile/resolve/pie_attachment.py +1 -1
  80. dataface/core/compile/resolve/scatter.py +25 -13
  81. dataface/core/compile/resolve/simple.py +25 -2
  82. dataface/core/compile/schema_renderers/prompt.py +1 -1
  83. dataface/core/compile/schema_renderers/textmate_grammar.py +8 -1
  84. dataface/core/compile/source_map.py +211 -28
  85. dataface/core/compile/style_cascade.py +126 -55
  86. dataface/core/compile/yaml_error_formatter.py +1 -1
  87. dataface/core/connections.py +10 -3
  88. dataface/core/dashboard.py +24 -18
  89. dataface/core/defaults/themes/_base.yaml +49 -39
  90. dataface/core/defaults/themes/cream.yaml +4 -4
  91. dataface/core/defaults/themes/diagnostics-title-baseline.yaml +1 -1
  92. dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +1 -1
  93. dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +1 -1
  94. dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +1 -1
  95. dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +1 -1
  96. dataface/core/defaults/themes/editorial.yaml +6 -6
  97. dataface/core/defaults/themes/neon.yaml +15 -8
  98. dataface/core/defaults/themes/plain.yaml +6 -4
  99. dataface/core/defaults/themes/stark.yaml +16 -7
  100. dataface/core/defaults/themes/vivid.yaml +15 -8
  101. dataface/core/diagnostics/__init__.py +2 -2
  102. dataface/core/diagnostics/codes_compile.py +13 -31
  103. dataface/core/diagnostics/codes_execute.py +79 -0
  104. dataface/core/diagnostics/codes_render.py +87 -28
  105. dataface/core/diagnostics/diagnostic.py +30 -0
  106. dataface/core/diagnostics/hints.py +19 -0
  107. dataface/core/dialects/base.py +17 -0
  108. dataface/core/dialects/postgres.py +14 -0
  109. dataface/core/dialects/redshift.py +5 -0
  110. dataface/core/execute/__init__.py +59 -14
  111. dataface/core/execute/adapters/dbt_adapter.py +4 -1
  112. dataface/core/execute/adapters/dbt_adapter_factory.py +19 -0
  113. dataface/core/execute/adapters/dbt_utils.py +84 -20
  114. dataface/core/execute/adapters/postgres_connection_manager.py +48 -0
  115. dataface/core/execute/adapters/sql_adapter.py +72 -4
  116. dataface/core/execute/dbt_jinja.py +65 -10
  117. dataface/core/execute/executor.py +69 -16
  118. dataface/core/font_measure.py +28 -0
  119. dataface/core/fonts.py +8 -5
  120. dataface/core/inspect/__init__.py +46 -6
  121. dataface/core/project.py +13 -1
  122. dataface/core/render/chart/data_table_attachment.py +14 -12
  123. dataface/core/render/chart/emitters/_cartesian.py +21 -18
  124. dataface/core/render/chart/emitters/_channels.py +56 -8
  125. dataface/core/render/chart/emitters/_label_overlap.py +18 -15
  126. dataface/core/render/chart/emitters/_measured_label_padding.py +4 -4
  127. dataface/core/render/chart/emitters/area.py +3 -3
  128. dataface/core/render/chart/emitters/bar.py +60 -35
  129. dataface/core/render/chart/emitters/geo.py +8 -13
  130. dataface/core/render/chart/emitters/heatmap.py +20 -26
  131. dataface/core/render/chart/emitters/line.py +2 -2
  132. dataface/core/render/chart/emitters/scatter.py +7 -30
  133. dataface/core/render/chart/features/baseline.py +22 -9
  134. dataface/core/render/chart/features/endpoint_labels.py +55 -32
  135. dataface/core/render/chart/features/mirror_axis.py +12 -9
  136. dataface/core/render/chart/features/value_labels.py +1 -1
  137. dataface/core/render/chart/kpi.py +11 -90
  138. dataface/core/render/chart/presentation.py +6 -8
  139. dataface/core/render/chart/rendering.py +3 -12
  140. dataface/core/render/chart/session.py +10 -0
  141. dataface/core/render/chart/spec.py +3 -0
  142. dataface/core/render/chart/spec_builders.py +18 -18
  143. dataface/core/render/chart/table.py +81 -36
  144. dataface/core/render/chart/table_support.py +2 -53
  145. dataface/core/render/chart/time_unit_detect.py +3 -2
  146. dataface/core/render/chart/translate.py +82 -44
  147. dataface/core/render/chart/type_inference.py +45 -25
  148. dataface/core/render/chart/validation.py +91 -0
  149. dataface/core/render/chart/vega_lite.py +30 -11
  150. dataface/core/render/chart/vl_field_maps.py +111 -76
  151. dataface/core/render/converters/chart.py +16 -6
  152. dataface/core/render/faces.py +3 -2
  153. dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.woff2 +0 -0
  154. dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.woff2 +0 -0
  155. dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -93
  156. dataface/core/render/fonts/_font_face.css +10 -0
  157. dataface/core/render/layout_sizing.py +267 -238
  158. dataface/core/render/numeral_expr.py +80 -0
  159. dataface/core/render/warnings/bar_band_width_too_narrow.py +64 -6
  160. dataface/core/render/warnings/layered_chart_shared_y_axis_scale_mismatch.py +25 -3
  161. dataface/core/render/warnings/likely_currency_or_percent_missing_formatter.py +12 -6
  162. dataface/core/render/warnings/palette_unsupported.py +6 -0
  163. dataface/core/render/warnings/pie_dominant_segment.py +1 -0
  164. dataface/core/render/warnings/pie_too_many_segments.py +1 -0
  165. dataface/core/render/warnings/point_map_out_of_projection.py +1 -0
  166. dataface/core/render/warnings/query_returned_zero_rows.py +1 -0
  167. dataface/core/render/warnings/redundant_encoding.py +20 -1
  168. dataface/core/render/warnings/registry.py +7 -3
  169. dataface/core/render/warnings/temporal_single_point.py +1 -0
  170. dataface/core/render/warnings/too_many_color_categories.py +1 -0
  171. dataface/core/render/warnings/too_many_x_categories.py +18 -2
  172. dataface/core/render/warnings/value_labels_crowd_width.py +9 -0
  173. dataface/core/render/warnings/y_encoding_mostly_null.py +1 -0
  174. dataface/core/render_format.py +18 -0
  175. dataface/core/text/format_d3.py +56 -0
  176. dataface/core/text/numeral_scale.py +218 -0
  177. dataface/core/utils.py +103 -10
  178. dataface/data/schemas/board-resolved/board-resolved.schema.json +9487 -7978
  179. dataface/data/schemas/yaml/0.3.2.json +1 -0
  180. dataface/data/schemas/yaml/manifest.json +7 -0
  181. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/METADATA +1 -1
  182. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/RECORD +185 -177
  183. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/WHEEL +0 -0
  184. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/entry_points.txt +0 -0
  185. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev111.dist-info}/licenses/LICENSE +0 -0
@@ -100,6 +100,58 @@ class MigrationRegistry:
100
100
  )
101
101
  return cls(moves, catalog=catalog)
102
102
 
103
+ @classmethod
104
+ def from_suffix_renames(
105
+ cls,
106
+ root_model: type[BaseModel],
107
+ source_schema: str,
108
+ target_schema: str,
109
+ renames: Iterable[tuple[YamlKeyPath, YamlKeyPath]],
110
+ *,
111
+ catalog: YamlSchemaCatalog,
112
+ ) -> MigrationRegistry:
113
+ """Build a move registry for tail-only path renames, one entry per
114
+ occurrence a shared style class is reused at.
115
+
116
+ ``MovedFrom`` (``from_model``) declares one fixed ``old_path`` per
117
+ field *declaration*, which is correct only for a field that occurs at
118
+ a single position in the tree. A cascade style class like
119
+ ``AxisLabelStyle`` is reused at hundreds of absolute positions (every
120
+ chart family's ``axis_x``/``axis_y``, every nested face, every theme
121
+ override) — one literal ``old_path`` cannot describe all of them.
122
+ ``renames`` instead pairs a *new-path tail* with the corresponding
123
+ *old-path tail*; this walks every occurrence of the new tail and
124
+ substitutes the old tail at that same position, keeping the shared
125
+ prefix. Candidates that don't structurally exist in both frozen
126
+ schemas (a coincidental tail match on an unrelated field, e.g. a mark
127
+ family also named ``line``) are silently dropped rather than passed
128
+ to ``Move`` — that keeps this safe to run over the whole field tree
129
+ without hand-enumerating every real position.
130
+ """
131
+ old_schema = catalog.schema_for(source_schema)
132
+ new_schema = catalog.schema_for(target_schema)
133
+ # Walk the field tree once and reuse it for every rename tuple —
134
+ # re-walking per rename is O(renames * tree_size) and the per-variant
135
+ # axis/scale models made tree_size large enough for that to matter.
136
+ paths = [path for path, _field in _walk_fields(root_model)]
137
+ seen: set[tuple[YamlKeyPath, YamlKeyPath]] = set()
138
+ moves: list[Move] = []
139
+ for new_tail, old_tail in renames:
140
+ width = len(new_tail)
141
+ for path in paths:
142
+ if len(path) < width or path[-width:] != new_tail:
143
+ continue
144
+ old_path = path[:-width] + old_tail
145
+ key = (old_path, path)
146
+ if key in seen:
147
+ continue
148
+ seen.add(key)
149
+ if _schema_path_exists(old_schema, old_path) and _schema_path_exists(
150
+ new_schema, path
151
+ ):
152
+ moves.append(Move(source_schema, target_schema, old_path, path))
153
+ return cls(moves, catalog=catalog)
154
+
103
155
  def transition_from(self, identifier: str) -> tuple[Move, ...]:
104
156
  if identifier not in self._moves_by_source:
105
157
  return ()
@@ -173,6 +225,37 @@ def migrate_face_yaml_text(yaml_text: str) -> str:
173
225
  return migrate_yaml_text(yaml_text, catalog=catalog, registry=registry)
174
226
 
175
227
 
228
+ # 0.3.1 -> 0.3.2: the axis/scale model split (per-variant AxisXStyle/AxisYStyle,
229
+ # BaseScaleStyle.continuous nesting) renamed several theme-cascade fields.
230
+ # Every rename here is a pure tail substitution — same shared prefix, only the
231
+ # trailing segment(s) change — so from_suffix_renames derives every occurrence
232
+ # a rename applies at. Leaf renames that land *inside* the "labels"/"scale"
233
+ # containers run before the "labels"/"line" container renames themselves: a
234
+ # leaf move creates its destination container on demand, and the later
235
+ # container move must find that destination still empty (or absent) to avoid
236
+ # a spurious MigrationConflictError against an unrelated field that also sets
237
+ # the old container. See dataface/AGENTS.md's "Two validation boundaries" —
238
+ # these are structural moves; categorical_orient (deleted), ticks.interval
239
+ # (reinterpreted vocabulary, not renamed), and the band/point/quantize scale
240
+ # groups (deleted) are semantic changes with no lossless move and are left to
241
+ # raise UnsupportedSchemaError for manual migration.
242
+ _AXIS_RESHAPE_RENAMES: tuple[tuple[YamlKeyPath, YamlKeyPath], ...] = (
243
+ (("labels", "format"), ("format",)),
244
+ (("scale", "values"), ("values",)),
245
+ (("scale", "continuous", "domain"), ("scale", "domain")),
246
+ (("scale", "continuous", "zero"), ("scale", "zero")),
247
+ (("scale", "continuous", "type"), ("scale", "type")),
248
+ (("scale", "continuous", "domain_strategy"), ("scale", "domain_strategy")),
249
+ (("scale", "continuous", "log", "base"), ("scale", "base")),
250
+ (("scale", "continuous", "pow", "exponent"), ("scale", "exponent")),
251
+ (("scale", "continuous", "symlog", "constant"), ("scale", "constant")),
252
+ (("scale", "padding"), ("scale", "band_padding_inner")),
253
+ (("ticks", "length"), ("ticks", "size")),
254
+ (("labels",), ("label",)),
255
+ (("line",), ("domain",)),
256
+ )
257
+
258
+
176
259
  @cache
177
260
  def _face_migration_context() -> tuple[YamlSchemaCatalog, MigrationRegistry]:
178
261
  """Load and validate the package's immutable migration metadata once."""
@@ -182,7 +265,17 @@ def _face_migration_context() -> tuple[YamlSchemaCatalog, MigrationRegistry]:
182
265
  )
183
266
 
184
267
  catalog = load_yaml_schema_catalog()
185
- registry = MigrationRegistry.from_model(AuthoredFace, catalog=catalog)
268
+ marker_registry = MigrationRegistry.from_model(AuthoredFace, catalog=catalog)
269
+ axis_reshape_registry = MigrationRegistry.from_suffix_renames(
270
+ AuthoredFace,
271
+ "0.3.1",
272
+ "0.3.2",
273
+ _AXIS_RESHAPE_RENAMES,
274
+ catalog=catalog,
275
+ )
276
+ registry = MigrationRegistry(
277
+ marker_registry.moves + axis_reshape_registry.moves, catalog=catalog
278
+ )
186
279
  return catalog, registry
187
280
 
188
281
 
@@ -223,14 +316,20 @@ def migrate_mapping(
223
316
  while identifier != current:
224
317
  moves = registry.transition_from(identifier)
225
318
  if not moves:
226
- current_errors = _current_schema_rejections(result, catalog)
227
- if not current_errors:
228
- break
229
- raise _unsupported_schema_error(current_errors[0])
319
+ break
230
320
  for move in moves:
231
321
  _apply_move(result, move)
232
322
  identifier = moves[0].target_schema
233
323
 
324
+ # A transition's declared moves may not cover every field a source schema
325
+ # allowed (a deleted field has no lossless Move — see MovedFrom's
326
+ # docstring). Recognizing an older schema and exhausting its transition
327
+ # chain is not proof the result is current-compatible; check explicitly
328
+ # rather than trust silently reaching the current identifier.
329
+ current_errors = _current_schema_rejections(result, catalog)
330
+ if current_errors:
331
+ raise _unsupported_schema_error(current_errors[0])
332
+
234
333
  if not allow_expired:
235
334
  warnings.warn(
236
335
  "Dataface migrated this YAML in memory; run `dft migrate` to update "
@@ -268,10 +367,7 @@ def migrate_yaml_text(
268
367
  while identifier != catalog.versions[0]:
269
368
  moves = registry.transition_from(identifier)
270
369
  if not moves:
271
- current_errors = _current_schema_rejections(staged, catalog)
272
- if not current_errors:
273
- break
274
- raise _unsupported_schema_error(current_errors[0])
370
+ break
275
371
  for move in moves:
276
372
  for parent, key, bindings in list(_source_locations(staged, move.old_path)):
277
373
  if isinstance(parent, list):
@@ -294,6 +390,13 @@ def migrate_yaml_text(
294
390
  removals.add(".".join(source))
295
391
  _apply_move(staged, move)
296
392
  identifier = moves[0].target_schema
393
+
394
+ # See migrate_mapping's identical check: a transition's declared moves
395
+ # may not cover every field the source schema allowed.
396
+ current_errors = _current_schema_rejections(staged, catalog)
397
+ if current_errors:
398
+ raise _unsupported_schema_error(current_errors[0])
399
+
297
400
  return set_face_values(
298
401
  yaml_text,
299
402
  {**updates, **dict.fromkeys(removals)},
@@ -7,7 +7,6 @@ from typing import Any, Literal
7
7
  from pydantic import Field
8
8
 
9
9
  from dataface.core.compile.models.chart.authored import KpiSupportConfig
10
- from dataface.core.compile.models.primitives import FormatConfig
11
10
  from dataface.core.compile.models.style.authored import KpiChartStylePatch
12
11
 
13
12
  from ._base import _BaseChartFields
@@ -34,9 +33,6 @@ class KpiChart(_BaseChartFields):
34
33
  default="stacked",
35
34
  description="Layout variant, passed through from authored KpiChart.variant.",
36
35
  )
37
- format: str | FormatConfig | None = Field(
38
- default=None, description="Number format override."
39
- )
40
36
  background: str | dict[str, Any] | None = Field(
41
37
  default=None,
42
38
  description=(
@@ -29,7 +29,7 @@ from dataface.core.compile.models.query.normalized import AnyQuery
29
29
  from dataface.core.compile.models.style.resolved import (
30
30
  ResolvedLegendStyle,
31
31
  )
32
- from dataface.core.compile.models.style.theme.board import PaddingStyle
32
+ from dataface.core.compile.models.style.theme.board import PaddingStyle, TitleStyle
33
33
  from dataface.core.compile.models.vega_lite.contracts import Projection
34
34
 
35
35
 
@@ -94,23 +94,41 @@ class _BaseResolvedChartFields(BaseModel):
94
94
  legend: ResolvedLegendStyle = Field(
95
95
  description="Baked board-level legend style from the style cascade.",
96
96
  )
97
- layout_padding: PaddingStyle | None = Field(
98
- default=None,
97
+ layout_padding: PaddingStyle = Field(
99
98
  description=(
100
- "Effective per-chart layout padding baked in at resolve time. "
101
- "Combines board.charts.padding with any chart-local style.<family>.padding "
102
- "override. None only for charts resolved outside the full pipeline "
103
- "(e.g. unit tests). render_layout_item falls back to board-level padding "
104
- "when None."
99
+ "Effective per-chart layout padding, baked in at construction time by "
100
+ "every family resolver. Combines board.charts.padding with any "
101
+ "chart-local style.<family>.padding override."
105
102
  ),
106
103
  )
107
104
 
108
105
 
109
106
  class _SharedResolvedChartFields(_BaseResolvedChartFields):
110
- """Adds title/subtitle for all families except KPI and Callout."""
107
+ """Adds title/subtitle/background for all families except KPI and Callout."""
111
108
 
112
- title: str | None = Field(default=None, description="Chart title.")
109
+ title: str | None = Field(
110
+ default=None,
111
+ description=(
112
+ "Jinja-resolved chart title, raw (no title-case transform). Also "
113
+ "backs non-VL consumers (e.g. the data-chart-title wire attribute) "
114
+ "that have always emitted the authored text as-is."
115
+ ),
116
+ )
113
117
  subtitle: str | None = Field(default=None, description="Chart subtitle.")
118
+ background: str = Field(
119
+ description=(
120
+ "Effective chart background: chart-local style.<family>.background "
121
+ "when authored, otherwise the board/face background."
122
+ ),
123
+ )
124
+ title_style: TitleStyle = Field(
125
+ description=(
126
+ "Effective title style — chart-local style.<family>.title merged "
127
+ "onto the board title style. Render-time VL assembly reads "
128
+ "title_style.font.case to case-transform `title` (VL presentation "
129
+ "only), and title_style.overflow for title-wrap mode."
130
+ ),
131
+ )
114
132
 
115
133
 
116
134
  class _CartesianResolvedChartFields(_SharedResolvedChartFields):
@@ -74,10 +74,9 @@ class ResolvedCalloutChart(BaseModel):
74
74
  default_factory=dict,
75
75
  description="Always empty — callout has no data channels.",
76
76
  )
77
- layout_padding: PaddingStyle | None = Field(
78
- default=None,
77
+ layout_padding: PaddingStyle = Field(
79
78
  description=(
80
- "Effective per-chart layout padding baked in at resolve time. "
79
+ "Effective per-chart layout padding, baked in at construction time. "
81
80
  "Structural parity with _BaseResolvedChartFields so render_layout_item "
82
81
  "can access chart.layout_padding without isinstance guards."
83
82
  ),
@@ -7,10 +7,10 @@ from typing import Literal
7
7
  from pydantic import Field
8
8
 
9
9
  from dataface.core.compile.models.chart.authored import KpiSupportConfig
10
+ from dataface.core.compile.models.primitives import FormatConfig
10
11
  from dataface.core.compile.models.style.resolved import ResolvedKpiStyle
11
12
 
12
13
  from ._base import _BaseResolvedChartFields
13
- from ._layer import FormatState
14
14
 
15
15
 
16
16
  class ResolvedKpiChart(_BaseResolvedChartFields):
@@ -50,9 +50,13 @@ class ResolvedKpiChart(_BaseResolvedChartFields):
50
50
  default="stacked",
51
51
  description="Layout variant — dispatches to a distinct SVG emit path.",
52
52
  )
53
- format: FormatState = Field(
53
+ format: FormatConfig | None = Field(
54
54
  default=None,
55
- description="D3-format string, FormatConfig, or None for the headline value.",
55
+ description=(
56
+ "Final headline value format — resolve() has already applied the "
57
+ "narrative-notation and SI-compaction defaults against this "
58
+ "chart's row; render consumes it as-is."
59
+ ),
56
60
  )
57
61
  style: ResolvedKpiStyle = Field(
58
62
  description="KPI family style slice.",
@@ -39,7 +39,15 @@ class ResolvedTableChart(_SharedResolvedChartFields):
39
39
  # not from source_chart.style (which no longer exists on Resolved*Chart).
40
40
  columns: dict[str, TableColumnConfig] | None = Field(
41
41
  default=None,
42
- description="Per-column display config (width, format, label, …).",
42
+ description=(
43
+ "Final per-column display config (width, format, label, …), keyed by "
44
+ "column name. Complete for every column resolve can see: defaults, "
45
+ "explicit overrides, and runtime FK links are already merged. Excludes "
46
+ "pivot measure columns (see `column_defaults`) — their real key space "
47
+ "only exists after render's pivot transform runs. None means no "
48
+ "column-level config or runtime link facts apply — render falls back "
49
+ "to plain query columns."
50
+ ),
43
51
  )
44
52
  header_overflow: Literal["clip", "truncate", "wrap-two", "wrap"] | None = Field(
45
53
  default=None,
@@ -47,7 +55,13 @@ class ResolvedTableChart(_SharedResolvedChartFields):
47
55
  )
48
56
  column_defaults: TableColumnDefaultsConfig | None = Field(
49
57
  default=None,
50
- description="Defaults applied to query-inferred columns.",
58
+ description=(
59
+ "Table-level column defaults, kept only for render's two render-native "
60
+ "column key spaces — pivot leaf/measure columns and transpose's "
61
+ "__metric__/__value__ pair — neither of which exists until a "
62
+ "render-time data transform runs. Every other column (explicit or "
63
+ "query-inferred) already has defaults merged into `columns`."
64
+ ),
51
65
  )
52
66
  style: ResolvedTableStyle = Field(
53
67
  description="Table family style slice.",
@@ -104,7 +104,7 @@ class SkipInheritSlots:
104
104
  ``_ChartStyleBase`` must not inherit from the root cascade.
105
105
 
106
106
  **On a leaf scalar field**: prevents the leaf from being added to the graph
107
- via slot expansion. Use on ``AxisStyle`` fields that the style resolver does
107
+ via slot expansion. Use on axis style fields that the style resolver does
108
108
  not cascade — e.g. ``grid.dash``, ``ticks.count``, ``scale``, ``position`` —
109
109
  so the inherit graph only declares what the resolver actually does.
110
110
 
@@ -8,12 +8,13 @@ cannot live in either without creating a circular import.
8
8
  from __future__ import annotations
9
9
 
10
10
  import re
11
- from typing import Annotated, Any, Literal
11
+ from typing import TYPE_CHECKING, Annotated, Any, Literal
12
12
 
13
13
  from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
14
14
 
15
15
  from dataface.core.compile.models.factories import (
16
16
  _PatchBase,
17
+ build_patch_model_ext,
17
18
  register_as_own_patch,
18
19
  register_patch,
19
20
  )
@@ -410,7 +411,158 @@ class FontColorStrokeStyle(StrokeStyle):
410
411
  )
411
412
 
412
413
 
413
- class ScaleTargetConfig(BaseModel):
414
+ # Vega/Vega-Lite's built-in named color schemes (https://vega.github.io/vega/docs/schemes/),
415
+ # the only strings ``ScaleTargetConfig.palette`` forwards straight through to VL's
416
+ # ``scale.scheme``. Sampled-discrete shorthand like "blues-9" is not a real scheme
417
+ # name — VL fails deep inside rendering ("Cannot read properties of undefined")
418
+ # rather than rejecting it up front, so this is validated at compile time instead.
419
+ #
420
+ # Empirically verified against this repo's pinned vl-convert (not hand-copied from
421
+ # docs alone): every name below was round-tripped through
422
+ # ``vlc.vegalite_to_svg`` with a quantitative ``scale.scheme`` encoding and
423
+ # confirmed to render — see ``test_vega_scheme_names_parity.py``, which re-runs
424
+ # that sweep and fails if this set and the bundled renderer ever diverge.
425
+ VEGA_SCHEME_NAMES: frozenset[str] = frozenset(
426
+ {
427
+ # Categorical
428
+ "accent",
429
+ "category10",
430
+ "category20",
431
+ "category20b",
432
+ "category20c",
433
+ "dark2",
434
+ "observable10",
435
+ "paired",
436
+ "pastel1",
437
+ "pastel2",
438
+ "set1",
439
+ "set2",
440
+ "set3",
441
+ "tableau10",
442
+ "tableau20",
443
+ # Sequential (single-hue)
444
+ "blues",
445
+ "tealblues",
446
+ "teals",
447
+ "greens",
448
+ "browns",
449
+ "greys",
450
+ "oranges",
451
+ "purples",
452
+ "reds",
453
+ "warmgreys",
454
+ # Sequential (multi-hue)
455
+ "viridis",
456
+ "magma",
457
+ "inferno",
458
+ "plasma",
459
+ "cividis",
460
+ "turbo",
461
+ "bluegreen",
462
+ "bluepurple",
463
+ "goldgreen",
464
+ "goldorange",
465
+ "goldred",
466
+ "greenblue",
467
+ "orangered",
468
+ "purplebluegreen",
469
+ "purpleblue",
470
+ "purplered",
471
+ "redpurple",
472
+ "yellowgreenblue",
473
+ "yellowgreen",
474
+ "yelloworangebrown",
475
+ "yelloworangered",
476
+ # Diverging
477
+ "blueorange",
478
+ "brownbluegreen",
479
+ "purplegreen",
480
+ "pinkyellowgreen",
481
+ "purpleorange",
482
+ "redblue",
483
+ "redgrey",
484
+ "redyellowblue",
485
+ "redyellowgreen",
486
+ "spectral",
487
+ # Cyclical
488
+ "rainbow",
489
+ "sinebow",
490
+ # For dark backgrounds
491
+ "darkblue",
492
+ "darkgold",
493
+ "darkgreen",
494
+ "darkmulti",
495
+ "darkred",
496
+ # For light backgrounds
497
+ "lightgreyred",
498
+ "lightgreyteal",
499
+ "lightmulti",
500
+ "lightorange",
501
+ "lighttealblue",
502
+ }
503
+ )
504
+
505
+
506
+ class ScaleTargetPaletteValidationMixin(BaseModel):
507
+ """Shared authored-input validator for ``ScaleTargetConfig.palette``.
508
+
509
+ Inherited by both ``ScaleTargetConfig`` and ``ScaleTargetConfigPatch`` so
510
+ chart-local authored patches reject a bad palette at the compile boundary
511
+ (where ``normalize_face()`` wraps the resulting ``pydantic.ValidationError``
512
+ into a proper ``ERR-VALIDATION-FIELD`` diagnostic) instead of only at
513
+ resolve time, where the raw pydantic error leaks past Dataface's error
514
+ formatting — ``build_patch_model_ext`` only carries over validators that
515
+ live on the patch model's base class (mirrors ``ScaleDomainValidationMixin``).
516
+ """
517
+
518
+ model_config = ConfigDict(extra="forbid")
519
+
520
+ @model_validator(mode="before")
521
+ @classmethod
522
+ def _validate_named_palette(cls, data: object) -> object:
523
+ """A string palette is either a literal Vega scheme name (forwarded
524
+ as-is to VL's ``scale.scheme`` by geo/heatmap gradient rendering) or a
525
+ Dataface named palette (resolved to hex stops downstream by
526
+ ``resolve_palette_stops``). Reject anything that resolves as neither.
527
+
528
+ A dash-suffixed name whose prefix is a real scheme (``"blues-9"``,
529
+ VL's sampled-discrete shorthand, which Dataface does not forward) gets
530
+ a specific hint naming the continuous scheme instead of the generic
531
+ unknown-palette message — that shape is the exact mistake this
532
+ validator exists to catch: an author reaching for N discrete buckets
533
+ that Dataface doesn't support yet, not a random typo.
534
+
535
+ ``mode="before"`` on the raw dict (not ``field_validator``) so this
536
+ runs unchanged on both ``ScaleTargetConfig`` and the generated
537
+ ``ScaleTargetConfigPatch`` — a field-scoped validator would need the
538
+ field to exist on this mixin itself, which a patch base never does.
539
+ """
540
+ if not isinstance(data, dict):
541
+ return data
542
+ value = data.get("palette")
543
+ if not isinstance(value, str) or value in VEGA_SCHEME_NAMES:
544
+ return data
545
+ from dataface.core.compile.palette import ( # noqa: PLC0415
546
+ UnknownPaletteError,
547
+ palette as resolve_palette,
548
+ )
549
+
550
+ try:
551
+ resolve_palette(value)
552
+ except UnknownPaletteError as exc:
553
+ base, _, suffix = value.rpartition("-")
554
+ if base in VEGA_SCHEME_NAMES and suffix.isdigit():
555
+ raise ValueError(
556
+ f"palette {value!r} requests a {suffix}-step discrete "
557
+ f"variant of the '{base}' scheme. Dataface does not support "
558
+ "bucketed/quantized color scales yet — use the continuous "
559
+ f"scheme '{base}' instead."
560
+ ) from None
561
+ raise ValueError(str(exc)) from None
562
+ return data
563
+
564
+
565
+ class ScaleTargetConfig(ScaleTargetPaletteValidationMixin):
414
566
  """Scale configuration for a single style target (background or color).
415
567
 
416
568
  Used as a chart channel annotation (``ColumnScaleConfig``) and as the
@@ -447,6 +599,27 @@ class ScaleTargetConfig(BaseModel):
447
599
  )
448
600
 
449
601
 
602
+ # Patch type for ScaleTargetConfig — generated (and registered) here, right after
603
+ # the compiled class, so every later recursive build_patch_model_ext call that
604
+ # encounters ScaleTargetConfig as a nested field (StaticGradientColorStyle.gradient,
605
+ # ColumnScaleConfig.color/background) reuses this exact registered class instead of
606
+ # a vanilla auto-generated one with no palette validation.
607
+ if TYPE_CHECKING:
608
+
609
+ class ScaleTargetConfigPatch(ScaleTargetConfig):
610
+ pass
611
+
612
+ else:
613
+
614
+ class _ScaleTargetConfigPatchBase(_PatchBase, ScaleTargetPaletteValidationMixin):
615
+ """Base for ScaleTargetConfigPatch: extra="forbid" from _PatchBase plus
616
+ the palette validation from ScaleTargetPaletteValidationMixin."""
617
+
618
+ ScaleTargetConfigPatch = register_patch(ScaleTargetConfig)(
619
+ build_patch_model_ext(ScaleTargetConfig, base_cls=_ScaleTargetConfigPatchBase)
620
+ )
621
+
622
+
450
623
  class CategoricalColorStyle(BaseModel):
451
624
  """Categorical palette config: per-series colors and single-series ink list."""
452
625
 
@@ -875,7 +875,11 @@ class RedshiftSourceConfig(
875
875
  ):
876
876
  """Redshift source configuration.
877
877
 
878
- Field names match dbt profiles.yml exactly.
878
+ Field names match dbt profiles.yml exactly, except `autocommit`: Dataface
879
+ only reads on this connection, and a non-autocommit session holds
880
+ AccessShareLock on every table it read until the connection closes,
881
+ blocking any writer that needs ACCESS EXCLUSIVE. `build_adapter` forces
882
+ it on unconditionally, so it isn't part of the authored surface.
879
883
 
880
884
  Read-only posture: no native driver flag. Bind to a SELECT-only IAM role
881
885
  or database user at the Redshift/IAM layer.
@@ -957,10 +961,6 @@ class RedshiftSourceConfig(
957
961
  str | None,
958
962
  Field(description="AWS region containing the Redshift target."),
959
963
  ] = None
960
- autocommit: Annotated[
961
- bool | None,
962
- Field(description="Whether Redshift statements autocommit."),
963
- ] = True
964
964
  access_key_id: Annotated[
965
965
  str | None,
966
966
  Field(description="AWS access key identifier."),
@@ -42,25 +42,36 @@ from pydantic import BaseModel, ConfigDict, Field, model_validator
42
42
  # Regression coverage: test_style_authored_bootstrap_order.py.
43
43
  from dataface.core.compile.models.style.authored._base import (
44
44
  AreaMarkStylePatch,
45
- AxisDomainStylePatch,
46
- AxisElementStylePatch,
47
45
  AxisGridStylePatch,
48
- AxisStylePatch,
46
+ AxisLabelStylePatch,
47
+ AxisLineStylePatch,
49
48
  AxisTicksStylePatch,
49
+ AxisTitleStylePatch,
50
+ AxisXStylePatch,
51
+ AxisYStylePatch,
52
+ BandAxisStylePatch,
50
53
  BarMarkStylePatch,
54
+ BaseAxisGridStylePatch,
55
+ BaseAxisStylePatch,
56
+ BaseScaleStylePatch,
51
57
  DataTableStylePatch,
58
+ DimensionLabelStylePatch,
59
+ DimensionTicksStylePatch,
52
60
  EndpointLabelsConfig,
53
61
  EndpointLabelsConfigPatch,
54
62
  GlobalMarksStylePatch,
55
63
  LegendStylePatch,
56
64
  LineMarkStylePatch,
65
+ MeasureGridStylePatch,
57
66
  PaddingStylePatch,
58
67
  PointMarkStylePatch,
59
- ScaleStylePatch,
68
+ QuantitativeAxisStylePatch,
69
+ ScaleContinuousStylePatch,
60
70
  StylePatch,
61
71
  TitleStylePatch,
62
72
  TooltipStylePatch,
63
73
  TotalStylePatch,
74
+ XScaleStylePatch,
64
75
  )
65
76
  from dataface.core.compile.models.style.authored.area import AreaChartStylePatch
66
77
  from dataface.core.compile.models.style.authored.bar import BarChartStylePatch
@@ -99,6 +110,7 @@ from dataface.core.compile.models.style.authored.table import (
99
110
  TableColumnConfig,
100
111
  TableColumnDefaultsConfig,
101
112
  _validate_glyph_pair as _validate_glyph_pair,
113
+ fill_table_column_defaults,
102
114
  )
103
115
 
104
116
  __all__ = [
@@ -106,11 +118,23 @@ __all__ = [
106
118
  "_validate_glyph_pair",
107
119
  "AreaChartStylePatch",
108
120
  "AreaMarkStylePatch",
109
- "AxisDomainStylePatch",
110
- "AxisElementStylePatch",
111
121
  "AxisGridStylePatch",
112
- "AxisStylePatch",
122
+ "AxisLabelStylePatch",
123
+ "AxisLineStylePatch",
113
124
  "AxisTicksStylePatch",
125
+ "AxisTitleStylePatch",
126
+ "AxisXStylePatch",
127
+ "AxisYStylePatch",
128
+ "BandAxisStylePatch",
129
+ "BaseAxisGridStylePatch",
130
+ "BaseAxisStylePatch",
131
+ "BaseScaleStylePatch",
132
+ "DimensionLabelStylePatch",
133
+ "DimensionTicksStylePatch",
134
+ "MeasureGridStylePatch",
135
+ "QuantitativeAxisStylePatch",
136
+ "ScaleContinuousStylePatch",
137
+ "XScaleStylePatch",
114
138
  "BarChartStylePatch",
115
139
  "BarMarkStylePatch",
116
140
  "CalloutChartStylePatch",
@@ -131,7 +155,6 @@ __all__ = [
131
155
  "PieChartStylePatch",
132
156
  "PointMapChartStylePatch",
133
157
  "PointMarkStylePatch",
134
- "ScaleStylePatch",
135
158
  "ScatterChartStylePatch",
136
159
  "SparkBarBarStylePatch",
137
160
  "SparkBarChartLabelStylePatch",
@@ -146,6 +169,7 @@ __all__ = [
146
169
  "TableColumnDefaultsConfig",
147
170
  "TitleStylePatch",
148
171
  "TooltipStylePatch",
172
+ "fill_table_column_defaults",
149
173
  "TotalStylePatch",
150
174
  ]
151
175