dataface 0.3.2.dev22__py3-none-any.whl → 0.3.2.dev150__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 (227) hide show
  1. dataface/DATAFACE_SYNTAX.md +15 -4
  2. dataface/__init__.py +70 -23
  3. dataface/agent_api/__init__.py +162 -68
  4. dataface/agent_api/_paths.py +9 -3
  5. dataface/agent_api/docs/yaml-reference.md +273 -239
  6. dataface/agent_api/init.py +1 -1
  7. dataface/agent_api/project_session.py +2 -1
  8. dataface/agent_api/serve.py +113 -0
  9. dataface/agent_api/validate.py +8 -0
  10. dataface/ai/agent.py +28 -2
  11. dataface/cli/_project.py +100 -1
  12. dataface/cli/commands/board_artifact.py +5 -2
  13. dataface/cli/commands/describe.py +7 -3
  14. dataface/cli/commands/docs.py +12 -3
  15. dataface/cli/commands/init.py +7 -14
  16. dataface/cli/commands/inspect.py +6 -1
  17. dataface/cli/commands/mcp_init.py +23 -43
  18. dataface/cli/commands/migrate.py +8 -3
  19. dataface/cli/commands/query.py +11 -8
  20. dataface/cli/commands/render.py +26 -9
  21. dataface/cli/commands/search.py +8 -2
  22. dataface/cli/commands/serve.py +25 -81
  23. dataface/cli/commands/skills_init.py +2 -19
  24. dataface/cli/commands/validate.py +7 -2
  25. dataface/cli/filesystem_project.py +5 -4
  26. dataface/cli/main.py +22 -33
  27. dataface/core/__init__.py +73 -28
  28. dataface/core/aliases.py +27 -0
  29. dataface/core/{compile/colors.py → colors.py} +20 -5
  30. dataface/core/compile/__init__.py +0 -8
  31. dataface/core/compile/channel.py +0 -13
  32. dataface/core/compile/compiler.py +161 -96
  33. dataface/core/compile/data_table.py +7 -2
  34. dataface/core/compile/enrich.py +18 -44
  35. dataface/core/compile/face_warnings.py +46 -8
  36. dataface/core/compile/format.py +47 -1
  37. dataface/core/compile/inherit_graph.py +3 -3
  38. dataface/core/compile/inherit_registry.yaml +1 -1
  39. dataface/core/compile/introspection.py +2 -13
  40. dataface/core/compile/migrations.py +112 -9
  41. dataface/core/compile/models/chart/authored/_base.py +1 -1
  42. dataface/core/compile/models/chart/normalized/__init__.py +2 -0
  43. dataface/core/compile/models/chart/normalized/_base.py +26 -0
  44. dataface/core/compile/models/chart/normalized/kpi.py +0 -4
  45. dataface/core/compile/models/chart/resolved/_base.py +28 -10
  46. dataface/core/compile/models/chart/resolved/callout.py +2 -3
  47. dataface/core/compile/models/chart/resolved/kpi.py +7 -3
  48. dataface/core/compile/models/chart/resolved/table.py +16 -2
  49. dataface/core/compile/models/config.py +5 -0
  50. dataface/core/compile/models/face/authored.py +24 -89
  51. dataface/core/compile/models/markers.py +1 -1
  52. dataface/core/compile/models/primitives.py +175 -2
  53. dataface/core/compile/models/query/authored.py +1 -119
  54. dataface/core/compile/models/refs.py +0 -2
  55. dataface/core/compile/models/source.py +78 -164
  56. dataface/core/compile/models/style/authored/__init__.py +32 -8
  57. dataface/core/compile/models/style/authored/_base.py +126 -69
  58. dataface/core/compile/models/style/authored/table.py +61 -3
  59. dataface/core/compile/models/style/resolved/__init__.py +12 -2
  60. dataface/core/compile/models/style/resolved/_base.py +142 -57
  61. dataface/core/compile/models/style/resolved/_cartesian.py +6 -11
  62. dataface/core/compile/models/style/resolved/_cascade.py +141 -54
  63. dataface/core/compile/models/style/resolved/area.py +1 -1
  64. dataface/core/compile/models/style/resolved/bar.py +1 -1
  65. dataface/core/compile/models/style/resolved/line.py +1 -1
  66. dataface/core/compile/models/style/resolved/scatter.py +3 -2
  67. dataface/core/compile/models/style/theme/__init__.py +36 -14
  68. dataface/core/compile/models/style/theme/_chart_base.py +17 -9
  69. dataface/core/compile/models/style/theme/axis.py +432 -312
  70. dataface/core/compile/models/style/theme/charts.py +10 -6
  71. dataface/core/compile/models/style/theme/kpi.py +2 -1
  72. dataface/core/compile/models/style/theme/page.py +5 -2
  73. dataface/core/compile/models/style/theme/table.py +2 -2
  74. dataface/core/compile/models/style/theme/variables.py +0 -3
  75. dataface/core/compile/models/vega_lite/config.py +1 -1
  76. dataface/core/compile/normalize_charts.py +10 -154
  77. dataface/core/compile/normalize_layout.py +20 -284
  78. dataface/core/compile/normalize_queries.py +24 -513
  79. dataface/core/compile/normalizer.py +4 -10
  80. dataface/core/compile/palette.py +19 -7
  81. dataface/core/compile/resolve/__init__.py +2 -0
  82. dataface/core/compile/resolve/_axes.py +147 -44
  83. dataface/core/compile/resolve/_channels.py +1 -1
  84. dataface/core/compile/resolve/_dispatch.py +1 -1
  85. dataface/core/compile/resolve/_domain.py +49 -32
  86. dataface/core/compile/resolve/_kwargs.py +39 -12
  87. dataface/core/compile/resolve/_marks.py +8 -3
  88. dataface/core/compile/resolve/_palette.py +7 -3
  89. dataface/core/compile/resolve/_table.py +124 -33
  90. dataface/core/compile/resolve/area.py +48 -18
  91. dataface/core/compile/resolve/bar.py +245 -64
  92. dataface/core/compile/resolve/geo.py +10 -3
  93. dataface/core/compile/resolve/heatmap.py +16 -8
  94. dataface/core/compile/resolve/line.py +35 -12
  95. dataface/core/compile/resolve/pie.py +4 -1
  96. dataface/core/compile/resolve/pie_attachment.py +1 -1
  97. dataface/core/compile/resolve/scatter.py +25 -13
  98. dataface/core/compile/resolve/simple.py +25 -2
  99. dataface/core/compile/schema_renderers/prompt.py +1 -1
  100. dataface/core/compile/schema_renderers/textmate_grammar.py +8 -1
  101. dataface/core/compile/source_map.py +211 -28
  102. dataface/core/compile/style_cascade.py +126 -55
  103. dataface/core/compile/validator.py +121 -44
  104. dataface/core/compile/yaml_error_formatter.py +108 -3
  105. dataface/core/connections.py +10 -3
  106. dataface/core/dashboard.py +27 -19
  107. dataface/core/defaults/default_config.yml +4 -0
  108. dataface/core/defaults/themes/_base.yaml +49 -40
  109. dataface/core/defaults/themes/cream.yaml +4 -4
  110. dataface/core/defaults/themes/diagnostics-title-baseline.yaml +1 -1
  111. dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +1 -1
  112. dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +1 -1
  113. dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +1 -1
  114. dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +1 -1
  115. dataface/core/defaults/themes/editorial.yaml +9 -6
  116. dataface/core/defaults/themes/neon.yaml +15 -8
  117. dataface/core/defaults/themes/plain.yaml +6 -4
  118. dataface/core/defaults/themes/stark.yaml +24 -7
  119. dataface/core/defaults/themes/vivid.yaml +15 -8
  120. dataface/core/diagnostics/__init__.py +12 -2
  121. dataface/core/diagnostics/codes_compile.py +17 -33
  122. dataface/core/diagnostics/codes_execute.py +149 -0
  123. dataface/core/diagnostics/codes_render.py +144 -31
  124. dataface/core/diagnostics/diagnostic.py +30 -0
  125. dataface/core/diagnostics/hints.py +19 -0
  126. dataface/core/dialects/base.py +17 -0
  127. dataface/core/dialects/postgres.py +14 -0
  128. dataface/core/dialects/redshift.py +5 -0
  129. dataface/core/execute/__init__.py +59 -14
  130. dataface/core/execute/adapters/__init__.py +1 -1
  131. dataface/core/execute/adapters/dbt_adapter.py +4 -1
  132. dataface/core/execute/adapters/dbt_adapter_factory.py +19 -0
  133. dataface/core/execute/adapters/dbt_utils.py +84 -20
  134. dataface/core/execute/adapters/postgres_connection_manager.py +48 -0
  135. dataface/core/execute/adapters/sql_adapter.py +72 -4
  136. dataface/core/execute/cache_keys.py +4 -9
  137. dataface/core/execute/dbt_jinja.py +65 -10
  138. dataface/core/execute/executor.py +94 -19
  139. dataface/core/execute/file_source_materializer.py +185 -32
  140. dataface/core/execute/trivial_local_cache.py +5 -2
  141. dataface/core/font_measure.py +28 -0
  142. dataface/core/fonts.py +8 -5
  143. dataface/core/inspect/__init__.py +46 -7
  144. dataface/core/project.py +19 -3
  145. dataface/core/render/chart/data_table_attachment.py +14 -12
  146. dataface/core/render/chart/emitters/_cartesian.py +21 -18
  147. dataface/core/render/chart/emitters/_channels.py +56 -8
  148. dataface/core/render/chart/emitters/_label_overlap.py +18 -15
  149. dataface/core/render/chart/emitters/_measured_label_padding.py +4 -4
  150. dataface/core/render/chart/emitters/area.py +3 -3
  151. dataface/core/render/chart/emitters/bar.py +60 -35
  152. dataface/core/render/chart/emitters/geo.py +8 -13
  153. dataface/core/render/chart/emitters/heatmap.py +20 -26
  154. dataface/core/render/chart/emitters/line.py +2 -2
  155. dataface/core/render/chart/emitters/scatter.py +7 -30
  156. dataface/core/render/chart/features/baseline.py +22 -9
  157. dataface/core/render/chart/features/endpoint_labels.py +55 -32
  158. dataface/core/render/chart/features/mirror_axis.py +12 -9
  159. dataface/core/render/chart/features/value_labels.py +1 -1
  160. dataface/core/render/chart/kpi.py +11 -90
  161. dataface/core/render/chart/mark_extents.py +339 -0
  162. dataface/core/render/chart/presentation.py +6 -8
  163. dataface/core/render/chart/rendering.py +100 -46
  164. dataface/core/render/chart/session.py +10 -0
  165. dataface/core/render/chart/spec.py +3 -0
  166. dataface/core/render/chart/spec_builders.py +18 -18
  167. dataface/core/render/chart/table.py +81 -36
  168. dataface/core/render/chart/table_support.py +2 -53
  169. dataface/core/render/chart/time_unit_detect.py +3 -2
  170. dataface/core/render/chart/translate.py +82 -44
  171. dataface/core/render/chart/type_inference.py +45 -25
  172. dataface/core/render/chart/validation.py +91 -0
  173. dataface/core/render/chart/vega_lite.py +30 -11
  174. dataface/core/render/chart/vl_field_maps.py +111 -76
  175. dataface/core/render/conditions.py +122 -0
  176. dataface/core/render/controls.py +60 -187
  177. dataface/core/render/converters/chart.py +16 -6
  178. dataface/core/render/data_format.py +1 -1
  179. dataface/core/render/face_resolve.py +7 -0
  180. dataface/core/render/faces.py +97 -32
  181. dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.woff2 +0 -0
  182. dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.woff2 +0 -0
  183. dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -93
  184. dataface/core/render/fonts/_font_face.css +10 -0
  185. dataface/core/render/layout_sizing.py +313 -238
  186. dataface/core/render/numeral_expr.py +80 -0
  187. dataface/core/render/renderer.py +14 -1
  188. dataface/core/render/sizing.py +42 -105
  189. dataface/core/render/svg_utils.py +60 -14
  190. dataface/core/render/terminal.py +1 -1
  191. dataface/core/render/variables_layout.py +211 -0
  192. dataface/core/render/variables_resolve.py +376 -0
  193. dataface/core/render/variables_strip.py +291 -368
  194. dataface/core/render/warnings/bar_band_width_too_narrow.py +64 -6
  195. dataface/core/render/warnings/base.py +18 -0
  196. dataface/core/render/warnings/layered_chart_shared_y_axis_scale_mismatch.py +25 -3
  197. dataface/core/render/warnings/layout_min_exceeds_height.py +68 -0
  198. dataface/core/render/warnings/likely_currency_or_percent_missing_formatter.py +12 -6
  199. dataface/core/render/warnings/palette_unsupported.py +6 -0
  200. dataface/core/render/warnings/pie_dominant_segment.py +1 -0
  201. dataface/core/render/warnings/pie_too_many_segments.py +1 -0
  202. dataface/core/render/warnings/point_map_out_of_projection.py +1 -0
  203. dataface/core/render/warnings/query_returned_zero_rows.py +1 -0
  204. dataface/core/render/warnings/redundant_encoding.py +20 -1
  205. dataface/core/render/warnings/registry.py +9 -3
  206. dataface/core/render/warnings/temporal_single_point.py +1 -0
  207. dataface/core/render/warnings/too_many_color_categories.py +1 -0
  208. dataface/core/render/warnings/too_many_x_categories.py +18 -2
  209. dataface/core/render/warnings/value_labels_crowd_width.py +9 -0
  210. dataface/core/render/warnings/y_encoding_mostly_null.py +1 -0
  211. dataface/core/render_format.py +18 -0
  212. dataface/core/serve/alias_index.py +16 -22
  213. dataface/core/text/format_d3.py +56 -0
  214. dataface/core/text/numeral_scale.py +218 -0
  215. dataface/core/utils.py +103 -10
  216. dataface/data/schemas/board-resolved/board-resolved.schema.json +8589 -7578
  217. dataface/data/schemas/yaml/0.3.2.json +1 -0
  218. dataface/data/schemas/yaml/manifest.json +7 -0
  219. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/METADATA +1 -3
  220. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/RECORD +223 -213
  221. dataface/core/inspect/partials/categorical.yml +0 -38
  222. dataface/core/inspect/partials/date.yml +0 -38
  223. dataface/core/inspect/partials/numeric.yml +0 -53
  224. dataface/core/inspect/templates/charts.yml +0 -94
  225. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/WHEEL +0 -0
  226. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/entry_points.txt +0 -0
  227. {dataface-0.3.2.dev22.dist-info → dataface-0.3.2.dev150.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,8 @@
1
- """Color utility functions for Dataface renderers.
1
+ """CSS/SVG color-string validation.
2
2
 
3
- Provides color helpers for use across compile and render layers.
3
+ Lives in core (below compile/execute/render) so both compile (pie-attachment
4
+ table style resolution) and render (KPI/table SVG emission) can validate a
5
+ foreign color string without either reaching into the other's layer.
4
6
  """
5
7
 
6
8
  from __future__ import annotations
@@ -8,11 +10,24 @@ from __future__ import annotations
8
10
  import re
9
11
  from typing import overload
10
12
 
11
- from dataface.core.compile.errors import CompilationError
13
+ from dataface.core.diagnostics.base import DatafaceError
12
14
 
13
15
  _CSS_HEX_COLOR_PATTERN = re.compile(r"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$")
14
16
 
15
17
 
18
+ class InvalidColorError(DatafaceError):
19
+ """A color string is neither a valid hex color nor a recognized CSS keyword."""
20
+
21
+ def __init__(self, message: str):
22
+ self.message = message
23
+ self.fields = {}
24
+ super().__init__(message)
25
+ if self.code is None:
26
+ from dataface.core.diagnostics.codes_unknown import ERR_INTERNAL
27
+
28
+ self.code = ERR_INTERNAL
29
+
30
+
16
31
  def is_sanitizable_color(color: str) -> bool:
17
32
  """Return True if ``color`` is accepted by sanitize_color (hex or transparent/none)."""
18
33
  return bool(_CSS_HEX_COLOR_PATTERN.match(color)) or color.lower() in {
@@ -39,7 +54,7 @@ def sanitize_color(color: str | None, fallback: str | None = None) -> str | None
39
54
  inherited fill).
40
55
 
41
56
  ``fallback`` is returned when ``color`` is None or empty. A non-None
42
- ``color`` that is not a valid hex or keyword raises ``CompilationError``
57
+ ``color`` that is not a valid hex or keyword raises ``InvalidColorError``
43
58
  immediately — authored colors that are not valid are a configuration error,
44
59
  not a case for silent defaulting.
45
60
  """
@@ -49,4 +64,4 @@ def sanitize_color(color: str | None, fallback: str | None = None) -> str | None
49
64
  return color
50
65
  if color.lower() in {"transparent", "none"}:
51
66
  return "transparent"
52
- raise CompilationError(f"Invalid color value: {color!r}")
67
+ raise InvalidColorError(f"Invalid color value: {color!r}")
@@ -80,8 +80,6 @@ from dataface.core.compile.models.face.normalized import (
80
80
  from dataface.core.compile.models.query.authored import (
81
81
  AuthoredCompactValuesQuery,
82
82
  AuthoredHttpQuery,
83
- AuthoredLookmlMergeQuery,
84
- AuthoredLookmlQuery,
85
83
  AuthoredMetricflowQuery,
86
84
  AuthoredQuery,
87
85
  AuthoredSchemaQuery,
@@ -104,8 +102,6 @@ from dataface.core.compile.models.source import (
104
102
  DuckDBSourceConfig,
105
103
  HttpSourceConfig,
106
104
  JsonSourceConfig,
107
- LookMLModelConfig,
108
- LookMLModelsMixin,
109
105
  MySQLSourceConfig,
110
106
  ParquetSourceConfig,
111
107
  PostgresSourceConfig,
@@ -162,8 +158,6 @@ __all__ = [
162
158
  "SparkBarChart",
163
159
  "AuthoredCompactValuesQuery",
164
160
  "AuthoredHttpQuery",
165
- "AuthoredLookmlQuery",
166
- "AuthoredLookmlMergeQuery",
167
161
  "AuthoredMetricflowQuery",
168
162
  "AuthoredQuery",
169
163
  "AuthoredSchemaQuery",
@@ -196,8 +190,6 @@ __all__ = [
196
190
  "JsonSourceConfig",
197
191
  "HttpSourceConfig",
198
192
  "DbtProfileSourceConfig",
199
- "LookMLModelConfig",
200
- "LookMLModelsMixin",
201
193
  "VALID_SOURCE_TYPES",
202
194
  "parse_source_config",
203
195
  "is_database_source",
@@ -10,7 +10,6 @@ block (indexed by column name), not per-channel. See ``types.py`` for
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
- import re
14
13
  from collections.abc import Callable, Iterable
15
14
  from dataclasses import dataclass
16
15
  from typing import Any, Protocol, runtime_checkable
@@ -72,8 +71,6 @@ class ConditionalFormattingChart(Protocol):
72
71
  ...
73
72
 
74
73
 
75
- _DOTTED_LOOKML_REF = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$")
76
-
77
74
  # Chart types whose color channel is a paintable mark fill — conditional_
78
75
  # formatting rules targeting `background` lower into their `color` channel.
79
76
  _MARK_FILL_CHART_TYPES: frozenset[str] = frozenset(
@@ -91,16 +88,6 @@ _MARK_FILL_CHART_TYPES: frozenset[str] = frozenset(
91
88
  )
92
89
 
93
90
 
94
- def is_dotted_lookml_ref(value: str) -> bool:
95
- """True if `value` has the shape of a LookML `view.field` ref.
96
-
97
- Exactly one dot, both sides valid identifiers, no spaces. Used by the
98
- Surface B collect pass to find channel values that name LookML fields
99
- rather than already-resolved column names.
100
- """
101
- return bool(_DOTTED_LOOKML_REF.match(value))
102
-
103
-
104
91
  def parse_style_channel(
105
92
  raw: _RawStyleChannel,
106
93
  channel_name: str,
@@ -66,12 +66,10 @@ from dataface.core.compile.models.chart.authored import _BaseChartFields
66
66
  from dataface.core.compile.models.config import ProjectCacheConfig
67
67
  from dataface.core.compile.models.face.authored import (
68
68
  AuthoredFace,
69
- ForeachItem,
70
69
  QueryOrRef,
71
70
  )
72
71
  from dataface.core.compile.models.face.normalized import Face
73
72
  from dataface.core.compile.models.query.authored import (
74
- AuthoredLookmlQuery,
75
73
  AuthoredMetricflowQuery,
76
74
  )
77
75
  from dataface.core.compile.models.query.normalized import AnyQuery, is_sql_query
@@ -83,10 +81,9 @@ from dataface.core.compile.models.refs import (
83
81
  )
84
82
  from dataface.core.compile.models.variable.authored import Variable
85
83
  from dataface.core.compile.normalize_charts import (
86
- collect_lookml_channel_refs,
87
84
  collect_metricflow_channel_refs,
88
85
  )
89
- from dataface.core.compile.normalize_queries import find_lookml_model_source
86
+ from dataface.core.compile.normalize_queries import split_external_query_ref
90
87
  from dataface.core.compile.normalize_variables import (
91
88
  VariableReferenceErrors,
92
89
  synthetic_query_name,
@@ -100,6 +97,7 @@ from dataface.core.compile.parser import (
100
97
  )
101
98
  from dataface.core.compile.source_map import (
102
99
  LiteralBlock,
100
+ build_container_paths,
103
101
  build_literal_blocks,
104
102
  build_source_map,
105
103
  stamp_diagnostics,
@@ -163,6 +161,12 @@ class CompileResult:
163
161
  diagnostics (chart-render errors, render warnings, query
164
162
  diagnostics) reuse this same map via ``stamp_diagnostics`` instead
165
163
  of each doing its own position resolution.
164
+ container_paths: Dotted paths addressing a mapping/sequence rather
165
+ than a scalar, built alongside ``source_map``. Lets the stamping
166
+ pass collapse a container's whole-body range to its key line, so a
167
+ diagnostic about a chart marks the line naming it instead of every
168
+ line inside. Carried on the result for the same reason as
169
+ ``sql_blocks``: render-side stamping reuses it without recompiling.
166
170
  sql_blocks: Dotted-path -> LiteralBlock for every literal (``|``)
167
171
  block scalar, built alongside ``source_map``. Render-side
168
172
  diagnostics pass this to ``stamp_diagnostics`` too, so a query
@@ -188,6 +192,7 @@ class CompileResult:
188
192
  meta_lint: MetaLintConfig | None = None
189
193
  source_map: dict[str, SourceRange] = field(default_factory=dict)
190
194
  sql_blocks: dict[str, LiteralBlock] = field(default_factory=dict)
195
+ container_paths: frozenset[str] = frozenset()
191
196
 
192
197
  @property
193
198
  def success(self) -> bool:
@@ -211,11 +216,13 @@ def _stamp_compile_result(
211
216
  return result
212
217
  source_map = build_source_map(yaml_content, file)
213
218
  sql_blocks = build_literal_blocks(yaml_content)
219
+ containers = build_container_paths(yaml_content)
214
220
  result.source_map = source_map
215
221
  result.sql_blocks = sql_blocks
216
- stamp_diagnostics(result.errors, source_map, sql_blocks)
217
- stamp_diagnostics(result.warnings, source_map, sql_blocks)
218
- stamp_diagnostics(result.suppressed_warnings, source_map, sql_blocks)
222
+ result.container_paths = containers
223
+ stamp_diagnostics(result.errors, source_map, sql_blocks, containers)
224
+ stamp_diagnostics(result.warnings, source_map, sql_blocks, containers)
225
+ stamp_diagnostics(result.suppressed_warnings, source_map, sql_blocks, containers)
219
226
  return result
220
227
 
221
228
 
@@ -315,7 +322,7 @@ def compile_authored_face(
315
322
  # ════════════════════════════════════════════════════════════════════
316
323
  # STEP 3b: Build Chart Registry (moved ahead of queries — Surface B's
317
324
  # collect pass below needs charts to find dotted-ref channel bindings
318
- # before lookml queries lower to SQL).
325
+ # before semantic queries lower to SQL).
319
326
  # ════════════════════════════════════════════════════════════════════
320
327
  # Pool that imported charts' own query chains resolve into, lexically
321
328
  # scoped to each chart's source face (compile/AGENTS.md's "Cross-face
@@ -342,11 +349,10 @@ def compile_authored_face(
342
349
  # ════════════════════════════════════════════════════════════════════
343
350
  # STEP 3b.5: Desugar chart-level `model:` sugar into a synthesized named
344
351
  # query, before Surface B channel collection (below) so the synthesized
345
- # query is visible to that pass. `model: model.explore` on a LookML source
346
- # becomes a lookml query; `model: source.semantic_model` on a dbt_profile
347
- # source becomes a metricflow query (roles inferred from the manifest in
348
- # STEP 3c). Runs unconditionally so an unknown/ambiguous `model:` name still
349
- # raises a clear error.
352
+ # query is visible to that pass. `model: source.semantic_model` on a
353
+ # dbt_profile source becomes a metricflow query (roles inferred from the
354
+ # manifest in STEP 3c). Runs unconditionally so an unknown/ambiguous
355
+ # `model:` name still raises a clear error.
350
356
  # ════════════════════════════════════════════════════════════════════
351
357
  metricflow_model_by_query: dict[str, str] = {}
352
358
  try:
@@ -375,33 +381,22 @@ def compile_authored_face(
375
381
  if len(_parts) != 2 or not all(_parts):
376
382
  raise CompilationError(
377
383
  f"chart '{_chart_key}' has `model: {_model_value!r}` — expected "
378
- "`model.explore` (a LookML model name and explore name "
379
- "joined by a dot)."
384
+ "`source.semantic_model` (a dbt_profile source name and "
385
+ "semantic model name joined by a dot)."
380
386
  )
381
- _model_name, _explore_name = _parts
382
- _lookml_source = find_lookml_model_source(_model_name, sources_registry)
383
- _source_cfg = sources_registry.get(_model_name)
384
- _is_metricflow = (
385
- _lookml_source is None
386
- and isinstance(_source_cfg, dict)
387
+ _source_name, _semantic_model_name = _parts
388
+ _source_cfg = sources_registry.get(_source_name)
389
+ if not (
390
+ isinstance(_source_cfg, dict)
387
391
  and _source_cfg.get("type") == "dbt_profile"
388
- )
389
- if _lookml_source is None and not _is_metricflow:
392
+ ):
390
393
  raise CompilationError(
391
394
  f"chart '{_chart_key}' has `model: {_model_value}` naming "
392
- f"unknown model '{_model_name}' — no source declares it in its "
393
- "'models:' map, and it is not a dbt_profile source."
395
+ f"unknown source '{_source_name}' — it is not a dbt_profile source."
394
396
  )
395
397
  _synth_name = synthetic_query_name("model_query", _chart_key)
396
- if _is_metricflow:
397
- authored.queries[_synth_name] = AuthoredMetricflowQuery(
398
- source=_model_name
399
- )
400
- metricflow_model_by_query[_synth_name] = _explore_name
401
- else:
402
- authored.queries[_synth_name] = AuthoredLookmlQuery(
403
- source=_model_name, explore=_explore_name
404
- )
398
+ authored.queries[_synth_name] = AuthoredMetricflowQuery(source=_source_name)
399
+ metricflow_model_by_query[_synth_name] = _semantic_model_name
405
400
  # Mutate in place: _BaseChartFields declares query + model; plain
406
401
  # dicts use key assignment. Non-_BaseChartFields BaseModels (e.g.
407
402
  # CalloutChart) have no `model:` field and are skipped above.
@@ -415,31 +410,21 @@ def compile_authored_face(
415
410
  return CompileResult(errors=[e.to_diagnostic(file=file)])
416
411
 
417
412
  # ════════════════════════════════════════════════════════════════════
418
- # STEP 3c: Collect dotted-ref (lookml) / bare-field (metricflow) chart
419
- # channels into their synthesized queries, before query normalization
420
- # lowers them. LookML collects `view.field` refs into a fields-less query;
421
- # MetricFlow classifies bare fields against the manifest into
413
+ # STEP 3c: Collect bare-field (metricflow) chart channels into their
414
+ # synthesized queries, before query normalization lowers them. MetricFlow
415
+ # classifies bare fields against the manifest into
422
416
  # metrics/dimensions/time_grain.
423
417
  # ════════════════════════════════════════════════════════════════════
424
- lookml_model_names: set[str] = set()
425
- for _cfg in sources_registry.values():
426
- if isinstance(_cfg, dict) and isinstance(_cfg.get("models"), dict):
427
- lookml_model_names.update(_cfg["models"].keys())
428
- if lookml_model_names or metricflow_model_by_query:
418
+ if metricflow_model_by_query:
429
419
  try:
430
420
  raw_queries: dict[str, QueryOrRef] = {}
431
421
  _collect_raw_queries(authored, raw_queries)
432
- if lookml_model_names:
433
- collect_lookml_channel_refs(
434
- collectible_charts, raw_queries, lookml_model_names
435
- )
436
- if metricflow_model_by_query:
437
- collect_metricflow_channel_refs(
438
- collectible_charts,
439
- raw_queries,
440
- metricflow_model_by_query,
441
- base_dir,
442
- )
422
+ collect_metricflow_channel_refs(
423
+ collectible_charts,
424
+ raw_queries,
425
+ metricflow_model_by_query,
426
+ base_dir,
427
+ )
443
428
  except CompilationError as e:
444
429
  return CompileResult(errors=[e.to_diagnostic(file=file)])
445
430
 
@@ -480,7 +465,7 @@ def compile_authored_face(
480
465
  # host-supplied fallback) so inline chart queries synthesized during
481
466
  # normalization inherit it too — not just the named queries built above.
482
467
  # `sources` (board-global named source configs) rides alongside so an
483
- # inline lookml/metricflow query on a chart resolves its model just
468
+ # inline metricflow query on a chart resolves its model just
484
469
  # like an up-top query.
485
470
  parent_context={
486
471
  "default_source": default_source,
@@ -983,11 +968,6 @@ def _extract_nested_faces(face: AuthoredFace) -> list[AuthoredFace]:
983
968
  tabs=item.tabs,
984
969
  )
985
970
  nested_faces.append(tab_face)
986
- elif isinstance(item, ForeachItem):
987
- # Recurse into foreach items to find any nested faces
988
- for foreach_subitem in item.foreach.items:
989
- if isinstance(foreach_subitem, AuthoredFace):
990
- nested_faces.append(foreach_subitem)
991
971
 
992
972
  return nested_faces
993
973
 
@@ -1057,7 +1037,7 @@ def _collect_raw_queries(face: AuthoredFace, registry: dict[str, QueryOrRef]) ->
1057
1037
 
1058
1038
  Unlike `build_query_registry`, entries stay as authored AuthoredQuery/
1059
1039
  QueryRef instances — used by the Surface B collect pass, which must
1060
- mutate a lookml query's `fields` before `normalize_query` lowers it.
1040
+ mutate a semantic query's fields before `normalize_query` lowers it.
1061
1041
  Cross-file QueryRef entries are included by reference but not resolved
1062
1042
  (a ref's own file already ran its own collect pass at its own compile).
1063
1043
  """
@@ -1075,7 +1055,7 @@ def _collect_inline_layout_charts(face: AuthoredFace, registry: dict[str, Any])
1075
1055
  `build_chart_registry` only sees top-level named `charts:` — a chart authored
1076
1056
  directly inline in `rows:`/`cols:`/`grid:`/`tabs:` (a bare AuthoredChart patch,
1077
1057
  or `dict[str, AuthoredChart]`) never gets a registry entry. The Surface B
1078
- collect pass (`collect_lookml_channel_refs`) needs to see and mutate those
1058
+ collect pass (`collect_metricflow_channel_refs`) needs to see and mutate those
1079
1059
  authored chart objects in place before normalization re-serializes them.
1080
1060
 
1081
1061
  Keys are Python object ids (`id(chart_obj)`) — guaranteed unique per live
@@ -1086,7 +1066,7 @@ def _collect_inline_layout_charts(face: AuthoredFace, registry: dict[str, Any])
1086
1066
  def register(item: object) -> None:
1087
1067
  # Handles both the bare-patch form (`- type: bar, ...`) and the named-dict
1088
1068
  # form (`- my_chart: {type: bar, ...}`). Anything else (chart-name string,
1089
- # nested face, foreach) is not an inline chart and is skipped here.
1069
+ # nested face, swatch strip) is not an inline chart and is skipped here.
1090
1070
  # str(id(item)) is unique per live Python object — safer key than a
1091
1071
  # position-only path, which collides when sibling nested faces share indices.
1092
1072
  if isinstance(item, _BaseChartFields):
@@ -1135,7 +1115,7 @@ def _process_queries_for_registry(
1135
1115
  registry: Registry to add queries to
1136
1116
  base_dir: ProjectDirectory anchor for the face file (used to resolve sub-file refs).
1137
1117
  default_source: Default source to apply to queries without explicit source
1138
- sources: Named source config dicts. Consulted by lookml queries at normalize time.
1118
+ sources: Named source config dicts. Consulted by metricflow queries at normalize time.
1139
1119
  face_cache: The face-scope cache layer already folded down the nesting
1140
1120
  chain by the caller.
1141
1121
 
@@ -1252,7 +1232,7 @@ def build_query_registry(
1252
1232
  registry: Existing registry to add to
1253
1233
  default_source: Default source to apply to queries without explicit source
1254
1234
  sources: Named source config dicts (project + face level). Consulted by
1255
- lookml queries, which resolve their source at normalize time.
1235
+ metricflow queries, which resolve their source at normalize time.
1256
1236
  face_cache: The cache layer this face inherits from the faces it is
1257
1237
  nested in; its own `cache:` folds over it inside `_build_registry`.
1258
1238
 
@@ -1397,6 +1377,26 @@ def _resolve_ref_item(
1397
1377
  return section[item_name], item_name, resolved_path, content
1398
1378
 
1399
1379
 
1380
+ def _face_level_default_source(
1381
+ content: dict[str, YamlValue], file_label: str
1382
+ ) -> str | None:
1383
+ """Validated face-level `source:` of a raw source-file mapping.
1384
+
1385
+ Cross-face-pulled queries inherit the declaring file's default source. The
1386
+ source file's content is a raw mapping here (never parsed into
1387
+ AuthoredFace), so the string-ness the model would enforce is checked here.
1388
+ """
1389
+ source = content.get("source")
1390
+ if source is None:
1391
+ return None
1392
+ if not isinstance(source, str):
1393
+ raise CompilationError(
1394
+ f"Invalid face-level `source:` in {file_label}: "
1395
+ f"expected a source name string, got {type(source).__name__}"
1396
+ )
1397
+ return source
1398
+
1399
+
1400
1400
  def load_from_reference(
1401
1401
  reference: VariableRef | QueryRef | ChartRef,
1402
1402
  base_dir: ProjectDirectory | None = None,
@@ -1451,7 +1451,7 @@ def load_from_reference(
1451
1451
  else:
1452
1452
  section_name = "charts"
1453
1453
 
1454
- item_def, item_name, resolved_path, _content = _resolve_ref_item(
1454
+ item_def, item_name, resolved_path, source_file_content = _resolve_ref_item(
1455
1455
  reference.ref, section_name, base_dir, yaml_path
1456
1456
  )
1457
1457
 
@@ -1459,6 +1459,7 @@ def load_from_reference(
1459
1459
  return normalize_query(
1460
1460
  item_name,
1461
1461
  item_def,
1462
+ _face_level_default_source(source_file_content, str(resolved_path.relpath)),
1462
1463
  base_dir=base_dir,
1463
1464
  sources=sources,
1464
1465
  cache_root=cache_root,
@@ -1523,20 +1524,60 @@ def pull_cross_face_query_chain(
1523
1524
 
1524
1525
 
1525
1526
  def _is_lexically_scoped_query_name(value: str) -> bool:
1526
- """True when a chart's `query:`/`layers[].query` string is a bare name
1527
- that should resolve against the source face's own `queries:` section,
1528
- rather than: inline SQL (self-contained), a `#`-external ref (separate,
1529
- pre-existing mechanism — resolving it correctly for imported charts is a
1530
- tracked follow-up, not this task), or a full `<file>.queries.<name>`
1531
- cross-file ref written directly on the chart itself (an already-
1532
- ambiguous, unrelated edge case for local charts too — left untouched).
1527
+ """True when a chart's `query:`/`layers[].query` string must resolve
1528
+ against the *source* face rather than the importer: a bare name declared
1529
+ in the source face's own `queries:` section, or a `<file>#<name>`
1530
+ external ref anchored at the source face's own directory. Excluded is
1531
+ inline SQL (self-contained) and a full `<file>.queries.<name>` cross-file
1532
+ ref written directly on the chart itself (an already-ambiguous, unrelated
1533
+ edge case for local charts too — left untouched).
1533
1534
  """
1534
1535
  stripped = value.strip()
1535
- return (
1536
- "#" not in stripped
1537
- and ".queries." not in stripped
1538
- and not looks_like_sql(stripped)
1539
- )
1536
+ return ".queries." not in stripped and not looks_like_sql(stripped)
1537
+
1538
+
1539
+ def _read_queries_section(
1540
+ file_dir: ProjectDirectory, target_file_str: str, ref: str, chart_ref: str
1541
+ ) -> tuple[ProjectPath, dict[str, YamlValue], dict[str, YamlValue]]:
1542
+ """The `queries:` section of `target_file_str`, resolved relative to
1543
+ `file_dir` — the *declaring* file's own directory, never the importer's.
1544
+
1545
+ Also returns the file's full parsed mapping, so callers can read its
1546
+ face-level `source:` default for the queries declared in it.
1547
+
1548
+ `ref` is the authored ref string this file part came from and `chart_ref`
1549
+ the chart import that pulled it in; both are named in every failure, since
1550
+ the author is looking at neither file when the resolution breaks.
1551
+ """
1552
+ where = f"Chart ref '{chart_ref}' requires '{ref}'"
1553
+ try:
1554
+ target_path = file_dir / target_file_str
1555
+ except ValueError as e:
1556
+ raise CompilationError(f"{where}: {e}") from e
1557
+ if not target_path.exists():
1558
+ raise CompilationError(
1559
+ f"{where}, but {target_file_str} does not exist "
1560
+ f"(resolved relative to {file_dir.relpath})."
1561
+ )
1562
+ try:
1563
+ target_content = target_path.read_yaml()
1564
+ except (OSError, yaml.YAMLError) as e:
1565
+ raise CompilationError(f"{where}: failed to load {target_file_str}: {e}") from e
1566
+ # read_yaml is -> Any: a file whose top level is a list or scalar parses
1567
+ # fine and would only fail at the .get below, as an AttributeError escaping
1568
+ # compile() rather than a diagnostic.
1569
+ if target_content is not None and not isinstance(target_content, dict):
1570
+ raise CompilationError(f"{where}, but {target_file_str} is not a YAML mapping.")
1571
+ if target_content is None:
1572
+ raise CompilationError(
1573
+ f"{where}, but {target_file_str} has no queries: section."
1574
+ )
1575
+ target_queries = target_content.get("queries")
1576
+ if not isinstance(target_queries, dict):
1577
+ raise CompilationError(
1578
+ f"{where}, but {target_file_str} has no queries: section."
1579
+ )
1580
+ return target_path, target_queries, target_content
1540
1581
 
1541
1582
 
1542
1583
  def _resolve_cross_face_query(
@@ -1549,6 +1590,7 @@ def _resolve_cross_face_query(
1549
1590
  resolving: set[tuple[str, str]],
1550
1591
  chart_ref: str,
1551
1592
  *,
1593
+ default_source: str | None,
1552
1594
  sources: dict[str, Any],
1553
1595
  cache_root: CachePatch | None,
1554
1596
  face_cache: CachePatch,
@@ -1593,19 +1635,9 @@ def _resolve_cross_face_query(
1593
1635
  target_file_str, target_name = raw_def["ref"].rsplit(".queries.", 1)
1594
1636
  if not (target_file_str.endswith(".yml") or target_file_str.endswith(".yaml")):
1595
1637
  target_file_str += ".yml"
1596
- try:
1597
- target_path = file_dir / target_file_str
1598
- except ValueError as e:
1599
- raise CompilationError(str(e)) from e
1600
- if not target_path.exists():
1601
- raise CompilationError(f"Referenced file not found: {target_file_str}")
1602
- try:
1603
- target_content = target_path.read_yaml()
1604
- except (OSError, yaml.YAMLError) as e:
1605
- raise CompilationError(f"Failed to load {target_file_str}: {e}") from e
1606
- target_queries = target_content.get("queries") if target_content else None
1607
- if not isinstance(target_queries, dict):
1608
- raise CompilationError(f"No queries found in {target_file_str}")
1638
+ target_path, target_queries, target_content = _read_queries_section(
1639
+ file_dir, target_file_str, raw_def["ref"], chart_ref
1640
+ )
1609
1641
  target_key = _resolve_cross_face_query(
1610
1642
  target_name,
1611
1643
  target_path.parent,
@@ -1615,6 +1647,7 @@ def _resolve_cross_face_query(
1615
1647
  key_cache,
1616
1648
  resolving,
1617
1649
  chart_ref,
1650
+ default_source=_face_level_default_source(target_content, target_file_str),
1618
1651
  sources=sources,
1619
1652
  cache_root=cache_root,
1620
1653
  face_cache=face_cache,
@@ -1640,6 +1673,7 @@ def _resolve_cross_face_query(
1640
1673
  key_cache,
1641
1674
  resolving,
1642
1675
  chart_ref,
1676
+ default_source=default_source,
1643
1677
  sources=sources,
1644
1678
  cache_root=cache_root,
1645
1679
  face_cache=face_cache,
@@ -1661,6 +1695,7 @@ def _resolve_cross_face_query(
1661
1695
  pool[qualified_key] = normalize_query(
1662
1696
  qualified_key,
1663
1697
  raw_def,
1698
+ default_source,
1664
1699
  base_dir=file_dir,
1665
1700
  sources=sources,
1666
1701
  cache_root=cache_root,
@@ -1710,19 +1745,49 @@ def _load_chart_ref_with_lexical_queries(
1710
1745
  queries_section = {}
1711
1746
  file_dir = resolved_path.parent
1712
1747
  file_relpath = resolved_path.relpath
1748
+ source_default = _face_level_default_source(content, str(file_relpath))
1713
1749
  key_cache: dict[tuple[str, str], str] = {}
1714
1750
  resolving: set[tuple[str, str]] = set()
1715
1751
 
1716
- def resolve_name(bare_name: str) -> str:
1752
+ def resolve_name(name: str) -> str:
1753
+ """A bare name resolves in the source face's own `queries:`; a
1754
+ `<file>#<name>` external ref resolves in that file's, reached relative
1755
+ to the source face's directory. Either way the resulting query lands
1756
+ in `cross_face_queries` under a key qualified by the file it was
1757
+ actually declared in — so an importer `#`-ref spelled identically to
1758
+ the imported chart's, but pointing at a different file, stays its own
1759
+ query.
1760
+ """
1761
+ stripped = name.strip()
1762
+ if "#" not in stripped:
1763
+ return _resolve_cross_face_query(
1764
+ stripped,
1765
+ file_dir,
1766
+ file_relpath,
1767
+ queries_section,
1768
+ cross_face_queries,
1769
+ key_cache,
1770
+ resolving,
1771
+ chart_ref.ref,
1772
+ default_source=source_default,
1773
+ sources=sources,
1774
+ cache_root=cache_root,
1775
+ face_cache=face_cache,
1776
+ )
1777
+ target_file_str, target_name = split_external_query_ref(stripped)
1778
+ target_path, target_queries, target_content = _read_queries_section(
1779
+ file_dir, target_file_str, stripped, chart_ref.ref
1780
+ )
1717
1781
  return _resolve_cross_face_query(
1718
- bare_name.strip(),
1719
- file_dir,
1720
- file_relpath,
1721
- queries_section,
1782
+ target_name,
1783
+ target_path.parent,
1784
+ target_path.relpath,
1785
+ target_queries,
1722
1786
  cross_face_queries,
1723
1787
  key_cache,
1724
1788
  resolving,
1725
1789
  chart_ref.ref,
1790
+ default_source=_face_level_default_source(target_content, target_file_str),
1726
1791
  sources=sources,
1727
1792
  cache_root=cache_root,
1728
1793
  face_cache=face_cache,
@@ -53,11 +53,16 @@ def axis_offset(charts_style: ResolvedChartsStyle, style: DataTableStyle) -> flo
53
53
  # Visibility is read from the fully-merged cascade (includes Layer-6 authored-
54
54
  # label flips) so both label and title heights reflect the actual rendered state.
55
55
  merged = resolved_axis_style(charts_style, "axis_x", "band")
56
+ # labels.padding is always required by build_resolved_axis (the shared
57
+ # ResolvedAxisElementStyle dataclass types it Optional only to let title
58
+ # leave it unset — see AxisTitleStyle).
59
+ assert merged.labels.padding is not None, "axis_x.labels.padding unset"
56
60
  label_h = (
57
- merged.label.padding + merged.label.font.size * style.label_max_lines
58
- if merged.label.visible is not False and merged.label.font.size > 0
61
+ merged.labels.padding + merged.labels.font.size * style.label_max_lines
62
+ if merged.labels.visible is not False and merged.labels.font.size > 0
59
63
  else 0.0
60
64
  )
65
+ assert merged.title.padding is not None, "axis_x.title.padding unset"
61
66
  title_h = (
62
67
  merged.title.padding + merged.title.font.size
63
68
  if merged.title.visible is not False and merged.title.font.size > 0