dataface 0.1.6.dev622__py3-none-any.whl → 0.1.6.dev644__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 (37) hide show
  1. dataface/DATAFACE_SYNTAX.md +5 -8
  2. dataface/agent_api/cache.py +3 -2
  3. dataface/agent_api/data_paths.py +10 -4
  4. dataface/agent_api/docs/yaml-reference.md +4 -33
  5. dataface/agent_api/project_session.py +1 -1
  6. dataface/agent_api/render_face.py +1 -1
  7. dataface/agent_api/validate.py +3 -1
  8. dataface/core/compile/detect.py +8 -1
  9. dataface/core/compile/models/chart/authored.py +51 -134
  10. dataface/core/compile/models/chart/normalized.py +53 -12
  11. dataface/core/compile/models/query/authored.py +2 -44
  12. dataface/core/compile/models/query/normalized.py +3 -13
  13. dataface/core/compile/normalize_charts.py +0 -14
  14. dataface/core/compile/normalize_variables.py +1 -7
  15. dataface/core/compile/schema_renderers/prompt.py +2 -2
  16. dataface/core/compile/schema_renderers/vscode_schema.py +0 -27
  17. dataface/core/execute/__init__.py +0 -2
  18. dataface/core/execute/_duckdb_cache_base.py +8 -13
  19. dataface/core/execute/cache_backend.py +7 -6
  20. dataface/core/execute/duckdb_cache.py +11 -285
  21. dataface/core/execute/executor.py +2 -218
  22. dataface/core/file_plugin.py +118 -0
  23. dataface/core/project.py +17 -39
  24. dataface/core/project_roots.py +10 -1
  25. dataface/core/render/chart/pipeline.py +14 -25
  26. dataface/core/render/chart/serialization.py +0 -5
  27. dataface/core/render/chart/table.py +352 -16
  28. dataface/core/render/chart/table_support.py +85 -31
  29. dataface/core/render/layout_sizing.py +10 -3
  30. dataface/core/serve/alias_index.py +16 -11
  31. dataface/core/serve/port.py +6 -1
  32. dataface/core/serve/server.py +6 -1
  33. {dataface-0.1.6.dev622.dist-info → dataface-0.1.6.dev644.dist-info}/METADATA +1 -1
  34. {dataface-0.1.6.dev622.dist-info → dataface-0.1.6.dev644.dist-info}/RECORD +37 -36
  35. {dataface-0.1.6.dev622.dist-info → dataface-0.1.6.dev644.dist-info}/WHEEL +0 -0
  36. {dataface-0.1.6.dev622.dist-info → dataface-0.1.6.dev644.dist-info}/entry_points.txt +0 -0
  37. {dataface-0.1.6.dev622.dist-info → dataface-0.1.6.dev644.dist-info}/licenses/LICENSE +0 -0
@@ -516,22 +516,20 @@ charts:
516
516
  y: total # column name OR [col, col, ...] for multi-series
517
517
  color: segment # column | {value: "#ccc"} | {field, scale} | {field, when}
518
518
 
519
- # Sizing — live at chart root, NOT under style:
519
+ # Sizing — height lives at chart root; aspect_ratio is a style field
520
520
  height: 400 # exact px; bypasses aspect_ratio and min/max clamps
521
- aspect_ratio: 2.0 # shape without a fixed size; height = width / aspect_ratio
522
- # height and aspect_ratio are ignored on kpi, table, callout, spark_bar
521
+ # height/aspect_ratio are ignored on kpi, table, callout, spark_bar
523
522
 
524
523
  # Style + behavior
525
524
  sort: { by: total, order: desc }
526
525
  x_label: "Month"
527
526
  y_label: "Revenue (USD)"
528
527
  link: "/orders?month={{ month }}" # Click-through URL template (drill-down)
529
- filters: { ... } # Result filters
530
528
 
531
529
  style: # Chart-local style patch (typed; not raw CSS) — paint only
532
- orientation: vertical # style: does NOT accept height or aspect_ratio
530
+ aspect_ratio: 2.0 # shape without a fixed size; height = width / aspect_ratio
533
531
  number_format: ",.0f" # D3 format string or named alias for axis/tooltip format
534
- stack: zero # none | zero | normalize | center
532
+ # bar/area families also accept style.orientation and style.stack
535
533
  ```
536
534
 
537
535
  ### Chart types (29 total)
@@ -585,7 +583,6 @@ All chart types accept the channels and style fields below — but each type rej
585
583
  | `background` | string \| object | Background channel — color, `{value}`, `{field, scale/when}`, or map layer |
586
584
  | `sort` | object | `{by, order}` — categorical sort. Horizontal bar charts default to value-descending order when omitted. |
587
585
  | `link` | string | Click-through URL template for drill-down links |
588
- | `filters` | object | Post-execution row filters: `{col: var_name}` (implicit `eq`) or `{col: {op: var}}` where op is one of `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `like`, `ilike`, `in`, `not_in`, `between`. Implicit-eq values may also be Jinja templates (e.g. `"{{ region }}"`) resolved at execute time. A filter is skipped when the variable is `None`, `""`, or renders to `"none"`. |
589
586
  | `layers` | list | Layer definitions for `type: layered` (see [Composition](#composition)) |
590
587
  | `conditional_formatting` | object | Discrete style rules by column (see [Conditional formatting](#conditional-formatting)) |
591
588
  | `data_table` | list | Attached mini-table beneath bar/line/area/layered (see [Composition](#composition)) |
@@ -598,7 +595,7 @@ All chart types accept the channels and style fields below — but each type rej
598
595
 
599
596
  KPI-only fields: `value`, `label`, `support`. KPI uses `label:` for the header text — `title:` is rejected on KPI charts. `glyph` and `tone` moved into the style namespace: use `style.glyph.character` and `style.tone`. To override glyph/value color, use `style.kpi.glyph.font.color` / `style.kpi.value.font.color`.
600
597
 
601
- Top-level chart fields shared by all types: `id`, `query`, `type`, `title`, `subtitle`, `description`, `height`, `aspect_ratio`, `style`, `link`, `filters`, `conditional_formatting`.
598
+ Top-level chart fields shared by all types: `id`, `query`, `type`, `title`, `subtitle`, `description`, `height`, `aspect_ratio`, `style`, `link`, `conditional_formatting`.
602
599
 
603
600
  ### Chart-type cheatsheet
604
601
 
@@ -3,13 +3,14 @@
3
3
  from collections.abc import Generator
4
4
  from contextlib import contextmanager
5
5
 
6
- from dataface.core.execute.duckdb_cache import DuckDBCache, open_cache_from_env
6
+ from dataface.core.execute.duckdb_cache import open_cache_from_env
7
+ from dataface.core.execute.trivial_local_cache import TrivialDuckDBCache
7
8
 
8
9
  __all__ = ["cache_from_env", "open_cache_from_env"]
9
10
 
10
11
 
11
12
  @contextmanager
12
- def cache_from_env() -> Generator[DuckDBCache | None]:
13
+ def cache_from_env() -> Generator[TrivialDuckDBCache | None]:
13
14
  """Open DFT_CACHE_PATH-backed cache (or None) and close it on exit.
14
15
 
15
16
  Use for CLI verbs and composition roots that need a cache scoped to
@@ -201,19 +201,25 @@ def build_alias_index_for_project(project: Project) -> AliasIndex:
201
201
  def data_alias_errors_for_file(
202
202
  face_file: Path,
203
203
  source_names: frozenset[str],
204
+ project: Project,
204
205
  ) -> list[str]:
205
206
  """Read aliases from a face file and lint any /data/ prefixed ones.
206
207
 
207
- Source-name check only no DB connection, no resolver. Called from the
208
- validate path which must stay connection-free.
208
+ Reads through *project* (the FilePlugin seam), so it works against a
209
+ git-blob store as well as the filesystem. Source-name check only — no DB
210
+ connection, no resolver. Called from the validate path which must stay
211
+ connection-free.
209
212
 
210
213
  Returns a list of error message strings; empty means no data alias issues.
211
214
  Silently returns [] when the file cannot be parsed (compile catches that).
212
215
  """
213
216
  from dataface.core.serve.alias_index import read_aliases_from_file
214
217
 
218
+ # file_for_path raises ValueError when face_file is outside project.root —
219
+ # that is a caller bug, not a parse error, and must NOT be swallowed.
220
+ project_file = project.file_for_path(face_file)
215
221
  try:
216
- aliases = read_aliases_from_file(face_file)
222
+ aliases = read_aliases_from_file(project_file)
217
223
  except ValueError:
218
- return [] # compile path reports alias parse errors with better context
224
+ return [] # compile path reports alias-shape parse errors with context
219
225
  return validate_data_aliases(aliases, source_names=source_names)
@@ -100,9 +100,8 @@ AuthoredQuery definition from YAML.
100
100
  | `rows` | list[dict[str, Any]] | ✓ | Inline data rows for values-type queries (list of row dicts). |
101
101
  | `values` | list[list[Any]] | ✓ | Inline column-oriented data for values-type queries (list of lists). |
102
102
  | `description` | str | ✓ | Human-readable description of the query. Used by AI search and tooling. |
103
- | `filters` | dict[str, Any] | ✓ | Jinja2 filter expressions applied to query results. |
103
+ | `filters` | dict[str, Any] | ✓ | Declarative column filters injected into the query's WHERE clause (SQL/DuckDB sources). |
104
104
  | `limit` | int | ✓ | Maximum number of rows returned. |
105
- | `pivot` | [Pivot](#pivot) | ✓ | Cross-tab pivot hint: transforms long-form SQL output into a wide table at render time (table consumers only). Does not affect SQL execution. |
106
105
  | `ignore` | list[str] | ✓ | Diagnostic codes to suppress for this query (e.g., ['fanout_risk', 'reaggregation']). |
107
106
 
108
107
  <a id="barchart"></a>
@@ -116,7 +115,6 @@ Authored patch for bar and histogram charts.
116
115
  | `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
117
116
  | `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
118
117
  | `link` | str | ✓ | Click-through URL template for drill-down links. |
119
- | `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
120
118
  | `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
121
119
  | `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
122
120
  | `title` | str | ✓ | Chart title displayed above the chart (not used on type: kpi). |
@@ -144,7 +142,6 @@ Authored patch for line charts.
144
142
  | `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
145
143
  | `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
146
144
  | `link` | str | ✓ | Click-through URL template for drill-down links. |
147
- | `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
148
145
  | `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
149
146
  | `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
150
147
  | `title` | str | ✓ | Chart title displayed above the chart (not used on type: kpi). |
@@ -172,7 +169,6 @@ Authored patch for area charts.
172
169
  | `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
173
170
  | `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
174
171
  | `link` | str | ✓ | Click-through URL template for drill-down links. |
175
- | `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
176
172
  | `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
177
173
  | `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
178
174
  | `title` | str | ✓ | Chart title displayed above the chart (not used on type: kpi). |
@@ -200,7 +196,6 @@ Authored patch for scatter charts.
200
196
  | `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
201
197
  | `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
202
198
  | `link` | str | ✓ | Click-through URL template for drill-down links. |
203
- | `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
204
199
  | `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
205
200
  | `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
206
201
  | `title` | str | ✓ | Chart title displayed above the chart (not used on type: kpi). |
@@ -230,7 +225,6 @@ Authored patch for heatmap charts.
230
225
  | `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
231
226
  | `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
232
227
  | `link` | str | ✓ | Click-through URL template for drill-down links. |
233
- | `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
234
228
  | `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
235
229
  | `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
236
230
  | `title` | str | ✓ | Chart title displayed above the chart (not used on type: kpi). |
@@ -259,7 +253,6 @@ Authored patch for pie and donut charts.
259
253
  | `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
260
254
  | `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
261
255
  | `link` | str | ✓ | Click-through URL template for drill-down links. |
262
- | `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
263
256
  | `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
264
257
  | `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
265
258
  | `title` | str | ✓ | Chart title displayed above the chart (not used on type: kpi). |
@@ -281,7 +274,6 @@ Authored patch for KPI (key performance indicator) charts.
281
274
  | `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
282
275
  | `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
283
276
  | `link` | str | ✓ | Click-through URL template for drill-down links. |
284
- | `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
285
277
  | `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
286
278
  | `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
287
279
  | `label` | str | ✓ | KPI label rendered above the headline value. |
@@ -300,12 +292,14 @@ Authored patch for table charts.
300
292
  | `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
301
293
  | `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
302
294
  | `link` | str | ✓ | Click-through URL template for drill-down links. |
303
- | `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
304
295
  | `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
305
296
  | `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
306
297
  | `title` | str | ✓ | Chart title displayed above the chart (not used on type: kpi). |
307
298
  | `subtitle` | str | ✓ | Chart subtitle displayed below the title. |
308
299
  | `style` | [TableChartStyle](#tablechartstyle) | ✓ | Chart-local style overrides. |
300
+ | `rows` | list[str] | ✓ | Fields whose distinct values form the row dimension of a pivot cross-tab. Each string is a column name from the query result. Omit for flat (non-pivot) tables. |
301
+ | `columns` | list[str] | ✓ | Fields whose distinct values become column headers in a pivot cross-tab. Multiple fields create a nested multi-dimension pivot (outer → inner). Omit for flat tables. |
302
+ | `values` | list[str] | ✓ | Measure fields that fill pivot cells. Each string is a column name from the query result. When omitted, all query columns not claimed by rows or columns are used. |
309
303
 
310
304
  <a id="pointmapchart"></a>
311
305
  ## PointMapChart
@@ -318,7 +312,6 @@ Authored patch for point_map and bubble_map charts.
318
312
  | `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
319
313
  | `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
320
314
  | `link` | str | ✓ | Click-through URL template for drill-down links. |
321
- | `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
322
315
  | `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
323
316
  | `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
324
317
  | `title` | str | ✓ | Chart title displayed above the chart (not used on type: kpi). |
@@ -346,7 +339,6 @@ Authored patch for map and geoshape charts.
346
339
  | `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
347
340
  | `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
348
341
  | `link` | str | ✓ | Click-through URL template for drill-down links. |
349
- | `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
350
342
  | `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
351
343
  | `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
352
344
  | `title` | str | ✓ | Chart title displayed above the chart (not used on type: kpi). |
@@ -373,7 +365,6 @@ Authored patch for layered multi-mark charts.
373
365
  | `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
374
366
  | `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
375
367
  | `link` | str | ✓ | Click-through URL template for drill-down links. |
376
- | `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
377
368
  | `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
378
369
  | `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
379
370
  | `title` | str | ✓ | Chart title displayed above the chart (not used on type: kpi). |
@@ -408,7 +399,6 @@ Authored patch for spark_bar charts (compact horizontal bars).
408
399
  | `description` | str | ✓ | Human-readable description used by AI search and context tooltips. |
409
400
  | `query` | str \| [Query](#query) \| QueryRef | ✓ | Named query reference, inline AuthoredQuery, or SQL string shorthand. |
410
401
  | `link` | str | ✓ | Click-through URL template for drill-down links. |
411
- | `filters` | dict[str, [FilterDef](#filterdef)] | ✓ | Declarative column filters applied to chart data after query execution. |
412
402
  | `conditional_formatting` | dict[str, [FieldConditionalFormatting](#fieldconditionalformatting)] | ✓ | Discrete rule-driven style overrides indexed by column name. |
413
403
  | `warnings_ignore` | list[str] | ✓ | Codes of render warnings to suppress for this chart. |
414
404
  | `title` | str | ✓ | Chart title displayed above the chart (not used on type: kpi). |
@@ -510,25 +500,6 @@ Options configuration for variable inputs.
510
500
  | `column` | str | ✓ | Column in the query result to use as option values. |
511
501
  | `label_column` | str | ✓ | Column in the query result to use as display labels (separate from values). |
512
502
 
513
- <a id="pivot"></a>
514
- ## Pivot
515
- Query-level pivot rendering hint for cross-tab table layout.
516
-
517
- | Field | Type | Description |
518
- |-------|------|-------------|
519
- | `column` | str | Dimension whose distinct values become column headers in the cross-tab layout. |
520
- | `value` | str | Measure that fills each cell in the cross-tab layout. |
521
-
522
- <a id="filterdef"></a>
523
- ## FilterDef
524
- One column→variable binding in chart.filters.
525
-
526
- | Field | Type | Optional | Description |
527
- |-------|------|:--------:|-------------|
528
- | `op` | enum: "eq", "neq", "gt", "gte", "lt", "lte", "like", "ilike", "in", "not_in", "between" | ✓ | Comparison operator. Defaults to 'eq'. |
529
- | `var` | str | ✓ | Plain variable name (no Jinja). Set when the filter value is a dashboard variable reference. |
530
- | `template` | str | ✓ | Jinja template resolved at execute time. Only valid with op='eq'. Set when conditional-disable or complex expressions are needed. |
531
-
532
503
  <a id="fieldconditionalformatting"></a>
533
504
  ## FieldConditionalFormatting
534
505
  Conditional formatting rules scoped to a single column.
@@ -124,7 +124,7 @@ class ProjectSession:
124
124
  """Construct a ProjectSession rooted at *project_dir*.
125
125
 
126
126
  Cache lifecycle belongs to the caller: pass ``cache=open_cache_from_env()``
127
- (or another DuckDBCache) when persistent caching is desired, otherwise omit
127
+ (or another cache backend) when persistent caching is desired, otherwise omit
128
128
  and the project will run uncached. ``close()`` does not touch the cache —
129
129
  whoever opened it closes it.
130
130
 
@@ -45,7 +45,7 @@ def render_face(
45
45
  use_cache: Whether to use the in-memory Executor result cache.
46
46
  cache: Optional DuckDB query-result cache. When provided, the caller
47
47
  owns the cache lifecycle and must close it. The DuckDB cache is
48
- *not* read from DFT_CACHE_PATH — pass an open DuckDBCache to
48
+ *not* read from DFT_CACHE_PATH — pass an open cache backend to
49
49
  enable caching (see ``open_cache_from_env`` in
50
50
  ``dataface.core.execute.duckdb_cache``).
51
51
  **options: Additional render options (e.g. ``scale`` for PNG).
@@ -258,7 +258,9 @@ def annotate_with_data_lint(
258
258
  if not result.path.exists() or not result.path.is_file():
259
259
  annotated.append(result)
260
260
  continue
261
- alias_msgs = data_alias_errors_for_file(result.path, source_names=source_names)
261
+ alias_msgs = data_alias_errors_for_file(
262
+ result.path, source_names=source_names, project=project_session.project
263
+ )
262
264
  if not alias_msgs:
263
265
  annotated.append(result)
264
266
  continue
@@ -60,7 +60,14 @@ def is_dataface_file(path: Path | str) -> bool:
60
60
  if "faces" in path.parts:
61
61
  return True
62
62
 
63
- # Content detection: check for Dataface-specific keys
63
+ # Content detection: check for Dataface-specific keys.
64
+ # TODO(fileplugin): this content branch (and is_markdown_face) reads the path
65
+ # off disk, bypassing the Project/FilePlugin seam. Enumeration already routes
66
+ # through project.iter_faces, but routing content detection through the seam
67
+ # means threading a ProjectFile here and into the path-based is_markdown_face
68
+ # / resolve_meta_chain helpers in compiler.py, plus keeping the VS Code
69
+ # extension's duplicated detection in sync. Deferred as a follow-up; faces
70
+ # discovered under faces/ or named *.dataface.yml never reach this branch.
64
71
  if path.exists():
65
72
  try:
66
73
  content = path.read_text(encoding="utf-8")
@@ -861,133 +861,6 @@ class Layer(BaseModel):
861
861
  return data
862
862
 
863
863
 
864
- # ============================================================================
865
- # FILTER BINDING
866
- # ============================================================================
867
-
868
- # Canonical set of recognized filter operator keys.
869
- # filter_injection._OP_MAP holds the sqlglot-class mapping for SQL injection;
870
- # this set is the authoritative list for compile-time validation.
871
- FILTER_OPS: frozenset[str] = frozenset(
872
- {"eq", "neq", "gt", "gte", "lt", "lte", "like", "ilike", "in", "not_in", "between"}
873
- )
874
-
875
-
876
- class FilterDef(BaseModel):
877
- """One column→variable binding in chart.filters.
878
-
879
- Authored in YAML as a plain variable name (implicit eq), a Jinja template
880
- (implicit eq, resolved at execute time), or a single-key operator dict:
881
-
882
- filters:
883
- region: selected_region → FilterDef(op="eq", var="selected_region")
884
- region: "{{ selected_region }}" → FilterDef(op="eq", template="{{ selected_region }}")
885
- revenue:
886
- gte: min_revenue → FilterDef(op="gte", var="min_revenue")
887
-
888
- Exactly one of ``var`` or ``template`` must be set. Operator dicts require
889
- plain variable names; Jinja templates are not accepted in operator-dict values.
890
- """
891
-
892
- model_config = ConfigDict(extra="forbid")
893
-
894
- op: Literal[
895
- "eq",
896
- "neq",
897
- "gt",
898
- "gte",
899
- "lt",
900
- "lte",
901
- "like",
902
- "ilike",
903
- "in",
904
- "not_in",
905
- "between",
906
- ] = Field(default="eq", description="Comparison operator. Defaults to 'eq'.")
907
- var: str | None = Field(
908
- default=None,
909
- description="Plain variable name (no Jinja). Set when the filter value is a dashboard variable reference.",
910
- )
911
- template: str | None = Field(
912
- default=None,
913
- description="Jinja template resolved at execute time. Only valid with op='eq'. Set when conditional-disable or complex expressions are needed.",
914
- )
915
-
916
- @model_validator(mode="before")
917
- @classmethod
918
- def _from_yaml(cls, v: Any) -> dict[str, Any]:
919
- """Coerce authored YAML shapes into normalized dict form.
920
-
921
- Accepts four input forms:
922
- - plain variable name: "region_var" → {op: "eq", var: "region_var"}
923
- - Jinja template: "{{ region_var }}" → {op: "eq", template: "{{ region_var }}"}
924
- - single-key op dict: {"gte": "min_rev"} → {op: "gte", var: "min_rev"}
925
- - already-normalized: {"op": X, "var": Y} — passed through as-is
926
- {"op": X, "template": Y} — passed through as-is
927
- """
928
- if isinstance(v, str):
929
- if not v:
930
- raise ValueError("filter value must be a non-empty string")
931
- if "{{" in v or "}}" in v:
932
- return {"op": "eq", "template": v}
933
- return {"op": "eq", "var": v}
934
- if isinstance(v, dict):
935
- # Already-normalized form from programmatic construction — pass through.
936
- if set(v.keys()) <= {"op", "var", "template"}:
937
- return v
938
- # YAML authored shape: single-key {operator: var_name} — plain var only.
939
- if len(v) != 1:
940
- raise ValueError(
941
- f"operator dict must have exactly one key, got {sorted(v.keys())!r}"
942
- )
943
- op, var = next(iter(v.items()))
944
- if op not in FILTER_OPS:
945
- raise ValueError(
946
- f"unknown operator {op!r}; must be one of {sorted(FILTER_OPS)}"
947
- )
948
- if not isinstance(var, str) or not var:
949
- raise ValueError(
950
- f"filters.{op}: variable name must be a non-empty string"
951
- )
952
- if "{{" in var or "}}" in var:
953
- raise ValueError(
954
- f"filters.{op}: Jinja templates are not allowed in operator-dict "
955
- f"filter values; use a plain variable name instead of {var!r}"
956
- )
957
- return {"op": op, "var": var}
958
- raise ValueError(
959
- f"filter binding must be a variable name string or single-key operator dict, "
960
- f"got {type(v).__name__}"
961
- )
962
-
963
- @model_validator(mode="after")
964
- def _exactly_one(self) -> FilterDef:
965
- """Exactly one of var or template must be set; template requires op='eq'."""
966
- if (self.var is None) == (self.template is None):
967
- raise ValueError(
968
- "FilterDef must set exactly one of var or template, "
969
- f"got var={self.var!r}, template={self.template!r}"
970
- )
971
- if self.template is not None and self.op != "eq":
972
- raise ValueError(
973
- f"FilterDef.template is only valid with op='eq', got op={self.op!r}"
974
- )
975
- return self
976
-
977
- def to_yaml_form(self) -> str | dict[str, str]:
978
- """Return the canonical YAML-authored representation.
979
-
980
- Template bindings and implicit-eq bindings round-trip as a plain string;
981
- all other operators round-trip as a single-key operator dict.
982
- """
983
- if self.template is not None:
984
- return self.template
985
- assert self.var is not None # enforced by _exactly_one
986
- if self.op == "eq":
987
- return self.var
988
- return {self.op: self.var}
989
-
990
-
991
864
  # ============================================================================
992
865
  # CHART FIELD BASES
993
866
  # ============================================================================
@@ -1076,13 +949,6 @@ class _BaseChartFields(BaseModel):
1076
949
  )
1077
950
  return data
1078
951
 
1079
- filters: Annotated[
1080
- dict[str, FilterDef] | None,
1081
- Field(
1082
- default=None,
1083
- description="Declarative column filters applied to chart data after query execution.",
1084
- ),
1085
- ]
1086
952
  conditional_formatting: Annotated[
1087
953
  dict[str, FieldConditionalFormatting] | None,
1088
954
  Field(
@@ -1619,6 +1485,57 @@ class TableChart(_SharedChartFields):
1619
1485
  TableChartStylePatch | None,
1620
1486
  Field(default=None, description="Chart-local style overrides."),
1621
1487
  ]
1488
+ rows: Annotated[
1489
+ list[str] | None,
1490
+ Field(
1491
+ default=None,
1492
+ description=(
1493
+ "Fields whose distinct values form the row dimension of a pivot cross-tab. "
1494
+ "Each string is a column name from the query result. "
1495
+ "Omit for flat (non-pivot) tables."
1496
+ ),
1497
+ ),
1498
+ ]
1499
+ columns: Annotated[
1500
+ list[str] | None,
1501
+ Field(
1502
+ default=None,
1503
+ description=(
1504
+ "Fields whose distinct values become column headers in a pivot cross-tab. "
1505
+ "Multiple fields create a nested multi-dimension pivot (outer → inner). "
1506
+ "Omit for flat tables."
1507
+ ),
1508
+ ),
1509
+ ]
1510
+ values: Annotated[
1511
+ list[str] | None,
1512
+ Field(
1513
+ default=None,
1514
+ description=(
1515
+ "Measure fields that fill pivot cells. "
1516
+ "Each string is a column name from the query result. "
1517
+ "When omitted, all query columns not claimed by rows or columns are used."
1518
+ ),
1519
+ ),
1520
+ ]
1521
+
1522
+ @model_validator(mode="after")
1523
+ def _validate_pivot_channels(self) -> TableChart:
1524
+ # A field may appear on at most one channel.
1525
+ seen: dict[str, str] = {}
1526
+ for channel, fields in (
1527
+ ("rows", self.rows or []),
1528
+ ("columns", self.columns or []),
1529
+ ("values", self.values or []),
1530
+ ):
1531
+ for f in fields:
1532
+ if f in seen:
1533
+ raise ValueError(
1534
+ f"Field {f!r} appears on both '{seen[f]}' and '{channel}' channels. "
1535
+ "A field may appear on at most one of rows/columns/values."
1536
+ )
1537
+ seen[f] = channel
1538
+ return self
1622
1539
 
1623
1540
 
1624
1541
  # --- Geo family ---
@@ -10,7 +10,7 @@ from __future__ import annotations
10
10
  from dataclasses import dataclass, field
11
11
  from typing import Any, Literal, TypeGuard
12
12
 
13
- from pydantic import BaseModel, ConfigDict, Field, field_validator
13
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
14
14
 
15
15
  from dataface.core.compile.models.chart.authored import (
16
16
  BUILTIN_CHART_TYPE_VALUES,
@@ -19,7 +19,6 @@ from dataface.core.compile.models.chart.authored import (
19
19
  ChartSort,
20
20
  ChartTotal,
21
21
  FieldConditionalFormatting,
22
- FilterDef,
23
22
  KpiSupportConfig,
24
23
  Layer,
25
24
  )
@@ -125,7 +124,7 @@ class Chart(BaseModel):
125
124
  # Variable dependencies - computed during normalization
126
125
  variable_dependencies: set[str] = Field(
127
126
  default_factory=set,
128
- description="Variable names this chart depends on (from title, filters, query SQL).",
127
+ description="Variable names this chart depends on (from title, subtitle, query SQL).",
129
128
  )
130
129
 
131
130
  # Source location in YAML for edit-back support
@@ -233,10 +232,6 @@ class Chart(BaseModel):
233
232
  link: str | None = Field(
234
233
  default=None, description="Click-through URL template for drill-down links."
235
234
  )
236
- filters: dict[str, FilterDef] | None = Field(
237
- default=None,
238
- description="Declarative column filters applied to chart data after query execution.",
239
- )
240
235
  layers: list[Layer] | None = Field(
241
236
  default=None, description="Layers for multi-mark charts (layered type)."
242
237
  )
@@ -284,6 +279,57 @@ class Chart(BaseModel):
284
279
  "Has no effect when chart.height is set. Promoted from per-chart style at compile time."
285
280
  ),
286
281
  )
282
+ # --- Pivot encoding channels (table charts only) ---
283
+ rows: list[str] | None = Field(
284
+ default=None,
285
+ description=(
286
+ "Fields whose distinct values form the row dimension of a pivot cross-tab. "
287
+ "None = flat table (no pivot)."
288
+ ),
289
+ )
290
+ # Justification for T | None: None means 'no pivot dimension' (flat table),
291
+ # which is a genuinely distinguishable authored state from an empty list.
292
+ columns: list[str] | None = Field(
293
+ default=None,
294
+ description=(
295
+ "Fields whose distinct values become column headers in a pivot cross-tab. "
296
+ "Multiple fields create a nested multi-dimension pivot (outer → inner). "
297
+ "None = flat table."
298
+ ),
299
+ )
300
+ # Justification for T | None: None means 'infer values:* at render time',
301
+ # which is semantically distinct from an explicit empty list.
302
+ values: list[str] | None = Field(
303
+ default=None,
304
+ description=(
305
+ "Measure fields that fill pivot cells. "
306
+ "None = infer as all query columns not claimed by rows or columns."
307
+ ),
308
+ )
309
+
310
+ @model_validator(mode="after")
311
+ def _validate_pivot_channels(self) -> Chart:
312
+ """Validate pivot channel invariants at the trust boundary.
313
+
314
+ Mirrors the authored-stage TableChart validator so the invariant also
315
+ holds for Chart objects built directly (tests, internal callers), not
316
+ only those that flow through AuthoredChart. Render trusts this.
317
+ """
318
+ seen: dict[str, str] = {}
319
+ for channel, fields in (
320
+ ("rows", self.rows or []),
321
+ ("columns", self.columns or []),
322
+ ("values", self.values or []),
323
+ ):
324
+ for f in fields:
325
+ if f in seen:
326
+ raise ValueError(
327
+ f"Field {f!r} appears on both {seen[f]!r} and {channel!r} "
328
+ "channels. A field may appear on at most one of rows/columns/values."
329
+ )
330
+ seen[f] = channel
331
+ return self
332
+
287
333
  warnings_ignore: list[str] = Field(
288
334
  default_factory=list,
289
335
  description="Codes of render warnings to suppress for this chart.",
@@ -380,11 +426,6 @@ class Chart(BaseModel):
380
426
  if self.link:
381
427
  result["link"] = self.link
382
428
 
383
- if self.filters:
384
- result["filters"] = {
385
- col: fd.to_yaml_form() for col, fd in self.filters.items()
386
- }
387
-
388
429
  return result
389
430
 
390
431
  def get_dependencies(self) -> ChartDependencies: