dataface 0.1.5.dev48__py3-none-any.whl → 0.1.5.dev151__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 (148) hide show
  1. dataface/DATAFACE_SYNTAX.md +10 -10
  2. dataface/_docs_site.py +1 -1
  3. dataface/agent_api/__init__.py +4 -6
  4. dataface/agent_api/_init_templates/README.md +3 -3
  5. dataface/agent_api/_init_templates/{guide.yml → guide.yaml} +3 -4
  6. dataface/agent_api/_paths.py +111 -63
  7. dataface/agent_api/dashboards.py +3 -2
  8. dataface/agent_api/describe.py +13 -8
  9. dataface/agent_api/describe_query.py +1 -1
  10. dataface/agent_api/docs/yaml-reference.md +254 -259
  11. dataface/agent_api/init.py +3 -36
  12. dataface/agent_api/inspect.py +4 -7
  13. dataface/agent_api/mcp_install.py +3 -3
  14. dataface/agent_api/project.py +224 -0
  15. dataface/agent_api/query.py +3 -2
  16. dataface/agent_api/surface_aliases.yaml +3 -3
  17. dataface/agent_api/validate.py +13 -8
  18. dataface/ai/mcp/server.py +4 -3
  19. dataface/ai/tools/__init__.py +16 -6
  20. dataface/cli/commands/describe.py +4 -1
  21. dataface/cli/commands/extension.py +21 -115
  22. dataface/cli/commands/init.py +13 -51
  23. dataface/cli/commands/inspect.py +2 -1
  24. dataface/cli/commands/mcp_init.py +23 -14
  25. dataface/cli/commands/query.py +54 -44
  26. dataface/cli/commands/render.py +18 -13
  27. dataface/cli/commands/serve.py +8 -9
  28. dataface/cli/commands/skills_init.py +3 -5
  29. dataface/cli/commands/validate.py +4 -1
  30. dataface/cli/main.py +47 -42
  31. dataface/core/compile/__init__.py +3 -3
  32. dataface/core/compile/chart_focus.py +5 -1
  33. dataface/core/compile/compiler.py +3 -4
  34. dataface/core/compile/config.py +17 -65
  35. dataface/core/compile/data_table_attachment.py +18 -18
  36. dataface/core/compile/introspection.py +9 -1
  37. dataface/core/compile/meta.py +2 -25
  38. dataface/core/compile/models/chart/authored.py +20 -240
  39. dataface/core/compile/models/chart/{compiled.py → normalized.py} +1 -1
  40. dataface/core/compile/{chart_resolved.py → models/chart/resolved.py} +9 -9
  41. dataface/core/compile/models/config.py +13 -10
  42. dataface/core/compile/models/face/{compiled.py → normalized.py} +7 -111
  43. dataface/core/compile/models/face/resolved.py +111 -0
  44. dataface/core/compile/models/palette.py +1 -1
  45. dataface/core/compile/models/primitives.py +1 -1
  46. dataface/core/compile/models/query/{compiled.py → normalized.py} +30 -13
  47. dataface/core/compile/models/refs.py +16 -10
  48. dataface/core/compile/models/style/authored.py +441 -11
  49. dataface/core/compile/models/style/{merged.py → resolved.py} +88 -86
  50. dataface/core/compile/models/style/{compiled.py → theme.py} +18 -205
  51. dataface/core/compile/models/variable/authored.py +27 -11
  52. dataface/core/compile/models/vega_lite/config.py +2 -14
  53. dataface/core/compile/normalize_charts.py +6 -4
  54. dataface/core/compile/normalize_layout.py +10 -6
  55. dataface/core/compile/normalize_queries.py +5 -2
  56. dataface/core/compile/normalize_variables.py +25 -7
  57. dataface/core/compile/normalizer.py +14 -14
  58. dataface/core/compile/palette.py +2 -2
  59. dataface/core/compile/parser.py +0 -1
  60. dataface/core/compile/schema_renderers/prompt.py +29 -11
  61. dataface/core/compile/sizing.py +29 -27
  62. dataface/core/compile/style_cascade.py +21 -21
  63. dataface/core/compile/typography.py +10 -6
  64. dataface/core/compile/validator.py +8 -7
  65. dataface/core/compile/variables.py +1 -1
  66. dataface/core/compile/vega_config.py +10 -27
  67. dataface/core/dashboard.py +5 -3
  68. dataface/core/defaults/default_config.yml +11 -0
  69. dataface/core/defaults/palettes/categorical/category-6-tonal-blue.yml +2 -2
  70. dataface/core/defaults/palettes/categorical/category-6-tonal-brown.yml +1 -1
  71. dataface/core/defaults/palettes/categorical/category-6-tonal-green.yml +1 -1
  72. dataface/core/defaults/palettes/categorical/category-6-tonal-orange.yml +1 -1
  73. dataface/core/defaults/palettes/categorical/category-6-tonal-purple.yml +1 -1
  74. dataface/core/execute/adapters/adapter_registry.py +9 -16
  75. dataface/core/execute/adapters/base.py +2 -2
  76. dataface/core/execute/adapters/csv_adapter.py +5 -5
  77. dataface/core/execute/adapters/dbt_adapter.py +14 -16
  78. dataface/core/execute/adapters/dbt_adapter_factory.py +8 -0
  79. dataface/core/execute/adapters/http_adapter.py +2 -2
  80. dataface/core/execute/adapters/metricflow_adapter.py +2 -2
  81. dataface/core/execute/adapters/schema_resolver_adapter.py +2 -2
  82. dataface/core/execute/adapters/sql_adapter.py +13 -17
  83. dataface/core/execute/adapters/values_adapter.py +2 -2
  84. dataface/core/execute/batch.py +3 -3
  85. dataface/core/execute/cache_keys.py +7 -2
  86. dataface/core/execute/duckdb_cache.py +2 -1
  87. dataface/core/execute/executor.py +6 -3
  88. dataface/core/execute/parallel.py +5 -2
  89. dataface/core/execute/setup_sql.py +1 -1
  90. dataface/core/execute/source_resolver.py +17 -4
  91. dataface/core/pack/models.py +1 -1
  92. dataface/core/project_roots.py +32 -51
  93. dataface/core/render/chart/callout.py +6 -6
  94. dataface/core/render/chart/decisions.py +1 -1
  95. dataface/core/render/chart/geo.py +11 -5
  96. dataface/core/render/chart/kpi.py +12 -9
  97. dataface/core/render/chart/pipeline.py +25 -19
  98. dataface/core/render/chart/profile.py +56 -45
  99. dataface/core/render/chart/render_single.py +6 -6
  100. dataface/core/render/chart/renderers.py +9 -9
  101. dataface/core/render/chart/rendering.py +19 -16
  102. dataface/core/render/chart/serialization.py +1 -1
  103. dataface/core/render/chart/spark.py +7 -7
  104. dataface/core/render/chart/spark_bar.py +6 -6
  105. dataface/core/render/chart/spec_builders.py +5 -3
  106. dataface/core/render/chart/standard_renderer.py +42 -54
  107. dataface/core/render/chart/table.py +22 -16
  108. dataface/core/render/chart/table_support.py +5 -3
  109. dataface/core/render/chart/type_inference.py +3 -3
  110. dataface/core/render/chart/validation.py +4 -1
  111. dataface/core/render/chart/vega_lite.py +9 -7
  112. dataface/core/render/chart/vega_lite_types.py +1 -1
  113. dataface/core/render/chart_interactivity.py +3 -3
  114. dataface/core/render/converters/chart.py +3 -3
  115. dataface/core/render/converters/html.py +4 -1
  116. dataface/core/render/faces.py +80 -26
  117. dataface/core/render/font_support.py +13 -44
  118. dataface/core/render/json_format.py +4 -1
  119. dataface/core/render/layout_sizing.py +9 -5
  120. dataface/core/render/layouts.py +11 -11
  121. dataface/core/render/placeholder.py +1 -1
  122. dataface/core/render/renderer.py +20 -2
  123. dataface/core/render/svg_utils.py +3 -3
  124. dataface/core/render/terminal.py +2 -2
  125. dataface/core/render/terminal_charts.py +1 -1
  126. dataface/core/render/terminal_layouts.py +4 -1
  127. dataface/core/render/text/case.py +3 -3
  128. dataface/core/render/text_format.py +4 -1
  129. dataface/core/render/utils.py +1 -1
  130. dataface/core/render/variable_controls.py +14 -9
  131. dataface/core/render/warnings/bar_color_1_to_1_with_x.py +3 -3
  132. dataface/core/render/warnings/base.py +1 -1
  133. dataface/core/render/warnings/redundant_encoding.py +69 -0
  134. dataface/core/render/warnings/registry.py +2 -0
  135. dataface/core/render/yaml_format.py +4 -1
  136. dataface/core/resolve_face.py +13 -13
  137. dataface/core/scoped_paths.py +14 -29
  138. dataface/core/serve/server.py +8 -1
  139. dataface/integrations/highlighting.py +112 -181
  140. dataface/integrations/markdown.py +4 -2
  141. {dataface-0.1.5.dev48.dist-info → dataface-0.1.5.dev151.dist-info}/METADATA +7 -8
  142. {dataface-0.1.5.dev48.dist-info → dataface-0.1.5.dev151.dist-info}/RECORD +145 -145
  143. dataface/agent_api/_init_templates/agents_dft_snippet.md +0 -26
  144. dataface/agent_api/_project_agents_md.py +0 -43
  145. dataface/core/compile/models/theme.py +0 -362
  146. {dataface-0.1.5.dev48.dist-info → dataface-0.1.5.dev151.dist-info}/WHEEL +0 -0
  147. {dataface-0.1.5.dev48.dist-info → dataface-0.1.5.dev151.dist-info}/entry_points.txt +0 -0
  148. {dataface-0.1.5.dev48.dist-info → dataface-0.1.5.dev151.dist-info}/licenses/LICENSE +0 -0
@@ -19,7 +19,7 @@ dft validate faces/my_dashboard.yml
19
19
  To verify that your database connection works, run a simple query:
20
20
 
21
21
  ```bash
22
- dft query 'SELECT 1' --source <your_source>
22
+ dft query <your_source> 'SELECT 1'
23
23
  ```
24
24
 
25
25
  A successful result means your data source is reachable.
@@ -138,7 +138,7 @@ variables:
138
138
  region:
139
139
  input: select # See `dft docs variables` for all 14 input types
140
140
  options: { static: [US, EU, APAC] }
141
- default: US
141
+ # No default: starts on All regions
142
142
  ```
143
143
 
144
144
  Reference variables inside queries with bare `{{ region }}` — no `variables.` prefix.
@@ -461,7 +461,6 @@ charts:
461
461
 
462
462
  # Style + behavior
463
463
  sort: { by: total, order: desc }
464
- stack: zero # false | zero | normalize | center
465
464
  format: integer # named alias or raw D3 spec (e.g. ",.0f")
466
465
  x_label: "Month"
467
466
  y_label: "Revenue (USD)"
@@ -470,7 +469,7 @@ charts:
470
469
 
471
470
  style: # Chart-local style patch (typed; not raw CSS) — paint only
472
471
  orientation: vertical # style: does NOT accept height or aspect_ratio
473
- stack: true
472
+ stack: zero # false | true | zero | normalize | center
474
473
  ```
475
474
 
476
475
  ### Chart types (29 total)
@@ -523,7 +522,6 @@ All chart types accept the channels and style fields below — but each type rej
523
522
  | `longitude` | string | Longitude field (point/bubble map) |
524
523
  | `background` | string \| object | Background channel — color, `{value}`, `{field, scale/when}`, or map layer |
525
524
  | `sort` | object | `{by, order}` — categorical sort |
526
- | `stack` | bool \| enum | `false`, `zero`, `normalize`, or `center` |
527
525
  | `link` | string | Click-through URL template for drill-down links |
528
526
  | `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"`. |
529
527
  | `layers` | list | Layer definitions for `type: layered` (see [Composition](#composition)) |
@@ -561,7 +559,8 @@ color: segment # Optional: one line per segment
561
559
  type: area
562
560
  x: date
563
561
  y: value
564
- stack: normalize # 100% stacked
562
+ style:
563
+ stack: normalize # 100% stacked
565
564
 
566
565
  # scatter — x and y numeric
567
566
  type: scatter
@@ -695,7 +694,6 @@ layers:
695
694
  - type: bar # bar | line | area | circle | square | tick | rule | trail | rect | image | scatter
696
695
  y: actual
697
696
  label: Actual
698
- fill: "#0369a1" # static paint (use fill:, not color: {value:})
699
697
  - type: line
700
698
  y: target
701
699
  label: Target
@@ -704,7 +702,7 @@ layers:
704
702
  title: "Target"
705
703
  ```
706
704
 
707
- Each layer accepts: `type`, `query` (optional layer-specific query), `x`, `y`, `label`, `color` (data channel, bare field name), `fill` (static hex paint for fixed-color marks), `size`, `shape`, `axis_y`. Vega-Lite `encoding:` is not allowed inside a layer — use the typed channels.
705
+ Each layer accepts: `type`, `query` (optional layer-specific query), `x`, `y`, `label`, `color` (data channel, bare field name), `axis_y`. Vega-Lite `encoding:` is not allowed inside a layer — use the typed channels.
708
706
 
709
707
  ### Conditional formatting
710
708
 
@@ -846,7 +844,7 @@ variables:
846
844
  description: "Restrict every query to one region."
847
845
  options:
848
846
  static: [US, EU, APAC]
849
- default: US
847
+ # No default: starts on All regions
850
848
  ```
851
849
 
852
850
  Input types (14 total):
@@ -894,7 +892,7 @@ variables:
894
892
  product:
895
893
  input: select
896
894
  options:
897
- static: [All, Electronics, Clothing] # Hardcoded list
895
+ static: [Electronics, Clothing] # Hardcoded list; blank -- All -- is implicit
898
896
  # OR
899
897
  query: products_list # Query whose first column is the option list
900
898
  column: product_name # Optional: which column in that query
@@ -907,6 +905,8 @@ variables:
907
905
 
908
906
  Top-level option-source binding (alternative to `options:`): `column`, `query`, `dimension` (MetricFlow), `measure` (MetricFlow), `model` (dbt).
909
907
 
908
+ For `select` and `multiselect`, omitting `default` starts the variable unset. The renderer adds a blank `-- All --` option for that unset state; `{{ filter('column', variable) }}` emits `1=1` unless the SQL call site opts into `none='deny'`.
909
+
910
910
  Disabled forms:
911
911
 
912
912
  ```yaml
dataface/_docs_site.py CHANGED
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
 
7
- DEFAULT_DOCS_SITE_URL = "https://docs.dataface.com"
7
+ DEFAULT_DOCS_SITE_URL = "https://docs.it-dataface.com"
8
8
 
9
9
 
10
10
  def docs_site_url() -> str:
@@ -37,10 +37,6 @@ CLI/MCP files that contain logic beyond parse-and-dispatch.
37
37
  """
38
38
 
39
39
  from dataface.agent_api import skills as skills
40
- from dataface.agent_api._paths import (
41
- RenderSetup as RenderSetup,
42
- setup_render_for_face as setup_render_for_face,
43
- )
44
40
  from dataface.agent_api.dashboards import (
45
41
  RenderedDashboard as RenderedDashboard,
46
42
  render_dashboard as render_dashboard,
@@ -55,8 +51,10 @@ from dataface.agent_api.init import (
55
51
  InitResult as InitResult,
56
52
  init_project as init_project,
57
53
  )
54
+ from dataface.agent_api.project import Project as Project
58
55
  from dataface.agent_api.query import QueryFaceResult, query_face
59
56
  from dataface.agent_api.validate import ValidateDashboardArgs, ValidateResult
57
+ from dataface.core.compile.config import ProjectSourcesConfig as ProjectSourcesConfig
60
58
 
61
59
  __all__ = [
62
60
  "DescribeFaceArgs",
@@ -65,8 +63,9 @@ __all__ = [
65
63
  "DocsResult",
66
64
  "DocsSearchHit",
67
65
  "InitResult",
66
+ "Project",
67
+ "ProjectSourcesConfig",
68
68
  "QueryFaceResult",
69
- "RenderSetup",
70
69
  "RenderedDashboard",
71
70
  "Topic",
72
71
  "ValidateDashboardArgs",
@@ -75,5 +74,4 @@ __all__ = [
75
74
  "init_project",
76
75
  "query_face",
77
76
  "render_dashboard",
78
- "setup_render_for_face",
79
77
  ]
@@ -10,7 +10,7 @@ for your data project.
10
10
 
11
11
  ## Authoring modes
12
12
 
13
- **YAML (`.yml`)** — structured dashboards with queries, charts, and layout.
13
+ **YAML (`.yaml`)** — structured dashboards with queries, charts, and layout.
14
14
  See the [dataface guide](guide) for a working example covering queries, charts, and variables.
15
15
 
16
16
  **Markdown (`.md`)** — prose pages like this one. Add YAML frontmatter for
@@ -18,7 +18,7 @@ queries and charts, then embed them inline with `{% raw %}{{ chart my_chart }}{%
18
18
 
19
19
  ## Next steps
20
20
 
21
- 1. Open `faces/guide.yml` and tweak the content.
21
+ 1. Open `faces/guide.yaml` and tweak the content.
22
22
  2. Run `dft serve` and open the URL it prints.
23
- 3. Add new `.yml` or `.md` files under `faces/` — they appear automatically.
23
+ 3. Add new `.yaml` or `.md` files under `faces/` — they appear automatically.
24
24
  4. Run `dft validate` to validate your face YAML for errors.
@@ -20,7 +20,7 @@ description: "A tour of Dataface: queries, charts, layout, KPIs, and variables."
20
20
  text: |
21
21
  Welcome to **Dataface** — dashboards as YAML files.
22
22
 
23
- This file is your starter guide. Edit it, add new `.yml` files under
23
+ This file is your starter guide. Edit it, add new `.yaml` files under
24
24
  `faces/`, and run `dft serve` to preview everything in your browser.
25
25
 
26
26
  # ── Queries ───────────────────────────────────────────────────────────────────
@@ -105,14 +105,13 @@ charts:
105
105
  # ── Variables ─────────────────────────────────────────────────────────────────
106
106
  # Variables add interactive dropdowns. Wire them to SQL queries with {{ varname }}.
107
107
  # With `type: values` data above they show the UI but don't filter — swap to SQL
108
- # queries and use `WHERE category = '{{ segment }}'` to make them live.
108
+ # queries and use `WHERE {{ filter('category', segment) }}` to make them live.
109
109
  variables:
110
110
  segment:
111
111
  label: Segment
112
112
  input: select
113
- default: All
114
113
  options:
115
- static: [All, Software, Services, Hardware]
114
+ static: [Software, Services, Hardware]
116
115
 
117
116
  # ── Layout ────────────────────────────────────────────────────────────────────
118
117
  # `rows:` stacks content vertically.
@@ -5,23 +5,67 @@ from __future__ import annotations
5
5
  from dataclasses import dataclass
6
6
  from pathlib import Path
7
7
 
8
- from dataface.core.execute.adapters import AdapterRegistry, build_adapter_registry
8
+ from dataface.core.execute.adapters import (
9
+ AdapterRegistry as AdapterRegistry,
10
+ build_adapter_registry as build_adapter_registry,
11
+ )
9
12
  from dataface.core.project_roots import (
10
- discover_render_context,
11
- discovery_boundary_for_face,
12
- find_dataface_project,
13
+ discover_render_context as discover_render_context,
14
+ discovery_boundary_for_face as discovery_boundary_for_face,
15
+ find_dft_root as find_dft_root,
13
16
  )
14
17
  from dataface.core.scoped_paths import (
15
- _resolve_against_project,
16
- project_root_for as project_root_for,
17
- resolve_scoped_path as resolve_scoped_path,
18
+ resolve_scoped_path as _core_resolve_scoped_path,
18
19
  )
19
20
 
20
21
 
22
+ def resolve_project_dir(project_dir: Path | None) -> Path:
23
+ """Resolve the caller's `project_dir`, walking up from cwd when omitted.
24
+
25
+ Boundary helper: every agent_api / CLI callsite that today forwards a
26
+ possibly-None `project_dir` into a core helper routes through this so the
27
+ cwd read stays at the agent_api boundary, not inside `dataface/core`.
28
+ """
29
+ if project_dir is not None:
30
+ return project_dir.resolve()
31
+ cwd = Path.cwd().resolve()
32
+ return find_dft_root(cwd) or cwd
33
+
34
+
35
+ def resolve_project_dir_from_paths(
36
+ paths: list[Path] | None, project_dir: Path | None
37
+ ) -> Path:
38
+ """Resolve project_dir for a multi-path CLI verb (validate / describe).
39
+
40
+ Explicit `--project-dir` wins. Otherwise, when all supplied paths are
41
+ absolute, walk up from the first path's parent to anchor on the project
42
+ that contains the file. Otherwise fall back to a cwd walk.
43
+ """
44
+ if project_dir is not None:
45
+ return project_dir.resolve()
46
+ if paths and all(p.is_absolute() for p in paths):
47
+ anchor = paths[0].parent.resolve()
48
+ return find_dft_root(anchor) or anchor
49
+ return resolve_project_dir(None)
50
+
51
+
52
+ def resolve_scoped_path(path: Path, project_dir: Path | None = None) -> Path:
53
+ """agent_api wrapper: resolves `None → cwd-walked root` at the boundary,
54
+ then delegates to `core.scoped_paths.resolve_scoped_path`.
55
+
56
+ Absolute paths with no explicit `project_dir` resolve directly (no
57
+ containment check) — the caller supplied the path, so no project context is
58
+ needed to validate containment.
59
+ """
60
+ if path.is_absolute() and project_dir is None:
61
+ return path.resolve()
62
+ return _core_resolve_scoped_path(path, resolve_project_dir(project_dir))
63
+
64
+
21
65
  def no_project_hint(project_dir: Path | None) -> str:
22
66
  """Return a hint string when no project marker is found, else empty string."""
23
67
  check = project_dir if project_dir is not None else Path.cwd()
24
- if find_dataface_project(check) is not None:
68
+ if find_dft_root(check) is not None:
25
69
  return ""
26
70
  return (
27
71
  f" No Dataface project marker found at or above {check}."
@@ -30,89 +74,93 @@ def no_project_hint(project_dir: Path | None) -> str:
30
74
 
31
75
 
32
76
  @dataclass(frozen=True)
33
- class RenderSetup:
34
- """Resolved arguments for an `agent_api.render_dashboard` call.
35
-
36
- Bundles the scoped path, scoped base, and adapter registry produced by
37
- discovery. CLI/serve callers building this from a face on disk avoid
38
- duplicating discovery boilerplate; tests can construct one directly.
39
- """
77
+ class FaceRenderContext:
78
+ """Resources resolved from a face path + project root for rendering."""
40
79
 
41
- adapter_registry: AdapterRegistry
80
+ face_file: Path
42
81
  scoped_path: Path | None
43
- scoped_base: Path | None
82
+ scoped_base: Path
83
+ project_root: Path
84
+ output_dir: Path
85
+ adapter_registry: AdapterRegistry
44
86
 
45
87
 
46
- def setup_render_for_face(
88
+ def build_face_render_context(
47
89
  face_path: Path,
48
90
  project_dir: Path | None = None,
49
91
  *,
50
92
  read_only: bool = True,
51
- ) -> tuple[RenderSetup, Path]:
52
- """Build a RenderSetup + resolved face_file for a face on disk.
93
+ ) -> FaceRenderContext:
94
+ """Resolve a face path, walk for dbt context, and build the adapter registry.
53
95
 
54
- Mirrors `core.render.face_api.render_face`'s discovery: walk upward from
55
- the face's parent to find dbt_project.yml and any sibling _sources.yaml.
56
- `project_dir` overrides project_root for project-level data paths but
57
- never the discovered dbt path.
96
+ ``project_dir=None`` means "walk freely from the face's parent" used by
97
+ the CLI when ``--project-dir`` is omitted so a face under a dbt sub-project
98
+ still anchors on that sub-project's root. A given ``project_dir`` is
99
+ authoritative; the walk only contributes the dbt project path.
58
100
  """
59
- face_file = face_path
60
- if not face_file.is_absolute():
61
- face_file = _resolve_against_project(face_file, project_dir)
101
+ if face_path.is_absolute():
102
+ face_file = face_path.resolve()
103
+ elif ".." in face_path.parts:
104
+ face_file = (Path.cwd() / face_path).resolve()
62
105
  else:
63
- face_file = face_file.resolve()
64
-
65
- project_root, dbt_project_path = discover_render_context(
106
+ anchor = (
107
+ project_dir.resolve()
108
+ if project_dir is not None
109
+ else resolve_project_dir(None)
110
+ )
111
+ face_file = (anchor / face_path).resolve()
112
+
113
+ walk_root, dbt_project_path = discover_render_context(
66
114
  face_file.parent,
67
115
  discovery_boundary_for_face(face_file.parent, project_dir),
68
116
  )
69
- if project_dir:
70
- project_root = project_dir.resolve()
117
+ project_root = project_dir.resolve() if project_dir is not None else walk_root
71
118
 
72
119
  try:
73
120
  scoped_path: Path | None = face_file.relative_to(project_root)
74
- scoped_base: Path | None = project_root
75
121
  except ValueError:
76
122
  scoped_path = face_file
77
- scoped_base = None
78
123
 
79
- adapter_registry = build_adapter_registry(
80
- project_root,
81
- read_only=read_only,
82
- dbt_project_path=dbt_project_path,
83
- )
84
- return (
85
- RenderSetup(
86
- adapter_registry=adapter_registry,
87
- scoped_path=scoped_path,
88
- scoped_base=scoped_base,
124
+ return FaceRenderContext(
125
+ face_file=face_file,
126
+ scoped_path=scoped_path,
127
+ scoped_base=project_root,
128
+ project_root=project_root,
129
+ output_dir=project_root,
130
+ adapter_registry=build_adapter_registry(
131
+ project_root, read_only=read_only, dbt_project_path=dbt_project_path
89
132
  ),
90
- face_file,
91
133
  )
92
134
 
93
135
 
94
- def setup_render_for_yaml(
136
+ @dataclass(frozen=True)
137
+ class YamlRenderContext:
138
+ """Resources resolved from a project root for rendering inline YAML."""
139
+
140
+ project_root: Path
141
+ output_dir: Path
142
+ adapter_registry: AdapterRegistry
143
+
144
+
145
+ def build_yaml_render_context(
95
146
  project_dir: Path | None = None,
96
147
  *,
97
148
  read_only: bool = True,
98
- ) -> RenderSetup:
99
- """Build a RenderSetup for a stdin/yaml_content render (no face path).
149
+ ) -> YamlRenderContext:
150
+ """Walk for dbt context and build the adapter registry for inline YAML.
100
151
 
101
- Walks upward from `project_dir` (or cwd) to discover the dbt project so
102
- yaml fed via stdin still resolves dbt-managed sources correctly.
152
+ ``project_dir=None`` walks from cwd to discover the project root; a given
153
+ ``project_dir`` is authoritative.
103
154
  """
104
- output_dir = project_root_for(project_dir)
105
- project_root, dbt_project_path = discover_render_context(output_dir, None)
106
- if project_dir:
107
- project_root = output_dir
108
-
109
- adapter_registry = build_adapter_registry(
110
- project_root,
111
- read_only=read_only,
112
- dbt_project_path=dbt_project_path,
155
+ anchor = (
156
+ project_dir.resolve() if project_dir is not None else resolve_project_dir(None)
113
157
  )
114
- return RenderSetup(
115
- adapter_registry=adapter_registry,
116
- scoped_path=None,
117
- scoped_base=project_root,
158
+ walk_root, dbt_project_path = discover_render_context(anchor, None)
159
+ project_root = anchor if project_dir is not None else walk_root
160
+ return YamlRenderContext(
161
+ project_root=project_root,
162
+ output_dir=project_root,
163
+ adapter_registry=build_adapter_registry(
164
+ project_root, read_only=read_only, dbt_project_path=dbt_project_path
165
+ ),
118
166
  )
@@ -108,7 +108,7 @@ class RenderDashboardArgs(BaseModel):
108
108
 
109
109
 
110
110
  def list_dashboards(
111
- directory: Path = Path("."),
111
+ directory: Path,
112
112
  recursive: bool = True,
113
113
  ) -> ListDashboardsResult:
114
114
  """List all available dashboards in a directory."""
@@ -197,8 +197,9 @@ def list_dashboards(
197
197
 
198
198
  def get_dashboard(
199
199
  path: Path,
200
+ *,
201
+ project_dir: Path,
200
202
  include_raw: bool = False,
201
- project_dir: Path | None = None,
202
203
  ) -> CompiledDashboard:
203
204
  """Get the compiled structure of a dashboard."""
204
205
  try:
@@ -7,11 +7,11 @@ from typing import Any
7
7
 
8
8
  from pydantic import BaseModel, Field
9
9
 
10
- from dataface.core.compile.models.chart.compiled import (
10
+ from dataface.core.compile.models.chart.normalized import (
11
11
  Chart,
12
12
  )
13
- from dataface.core.compile.models.face.compiled import Layout
14
- from dataface.core.compile.models.query.compiled import (
13
+ from dataface.core.compile.models.face.normalized import Layout
14
+ from dataface.core.compile.models.query.normalized import (
15
15
  CsvQuery,
16
16
  DbtModelQuery,
17
17
  HttpQuery,
@@ -84,7 +84,11 @@ class DescribeFaceArgs(BaseModel):
84
84
 
85
85
  path: Path = Field(description="Path to the dashboard YAML file to describe")
86
86
  project_dir: Path | None = Field(
87
- None, description="Project root for resolving relative paths"
87
+ default=None,
88
+ description=(
89
+ "Project root for resolving relative paths. Optional on the wire; "
90
+ "the MCP server injects ctx.default_project_dir or cwd when omitted."
91
+ ),
88
92
  )
89
93
 
90
94
 
@@ -186,7 +190,7 @@ def _encoding_for_chart(chart: Chart) -> dict[str, Any]:
186
190
  # ---------------------------------------------------------------------------
187
191
 
188
192
 
189
- def describe_face(path: Path, project_dir: Path | None = None) -> DescribeFaceResult:
193
+ def describe_face(path: Path, *, project_dir: Path) -> DescribeFaceResult:
190
194
  """Describe the structure of a face: queries, charts, variables, layout."""
191
195
  from dataface.agent_api._paths import no_project_hint, resolve_scoped_path
192
196
  from dataface.core.compile.compiler import compile_file
@@ -300,7 +304,8 @@ def describe_face(path: Path, project_dir: Path | None = None) -> DescribeFaceRe
300
304
 
301
305
  def describe_paths(
302
306
  paths: list[Path],
303
- project_dir: Path | None = None,
307
+ *,
308
+ project_dir: Path,
304
309
  ) -> list[DescribeFaceResult]:
305
310
  """Describe N face files / directories.
306
311
 
@@ -310,13 +315,13 @@ def describe_paths(
310
315
  """
311
316
  out: list[DescribeFaceResult] = []
312
317
  for p in paths:
313
- out.extend(_describe_one_path(p, project_dir=project_dir))
318
+ out.extend(_describe_one_path(p, project_dir))
314
319
  return out
315
320
 
316
321
 
317
322
  def _describe_one_path(
318
323
  path: Path,
319
- project_dir: Path | None = None,
324
+ project_dir: Path,
320
325
  ) -> list[DescribeFaceResult]:
321
326
  """Per-argv expansion: file → [one], dir → walk."""
322
327
  # WHY: dataface.core.inspect.manifest_utils triggers the inspect package
@@ -109,7 +109,7 @@ def _duckdb_describe(
109
109
  sql: str, source: str | None, adapter_registry: AdapterRegistry
110
110
  ) -> list[DescribeQueryColumn]:
111
111
  """Run DESCRIBE ({sql}) via the registry's read-only SqlAdapter."""
112
- from dataface.core.compile.models.query.compiled import SqlQuery
112
+ from dataface.core.compile.models.query.normalized import SqlQuery
113
113
 
114
114
  result = adapter_registry.execute(SqlQuery(sql=f"DESCRIBE ({sql})", source=source))
115
115
  if result.error: