dataface 0.1.6.dev76__py3-none-any.whl → 0.1.6.dev129__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 (53) hide show
  1. dataface/agent_api/_paths.py +7 -18
  2. dataface/agent_api/data_paths.py +6 -12
  3. dataface/agent_api/docs/yaml-reference.md +13 -12
  4. dataface/agent_api/files.py +17 -13
  5. dataface/agent_api/pack.py +2 -2
  6. dataface/agent_api/project_session.py +7 -44
  7. dataface/agent_api/render_face.py +1 -14
  8. dataface/agent_api/validate.py +1 -1
  9. dataface/ai/context.py +2 -1
  10. dataface/ai/tools/__init__.py +5 -5
  11. dataface/cli/commands/render.py +2 -10
  12. dataface/cli/commands/schema.py +23 -24
  13. dataface/core/compile/compiler.py +14 -10
  14. dataface/core/compile/config.py +10 -5
  15. dataface/core/compile/detect.py +32 -6
  16. dataface/core/compile/markdown.py +109 -18
  17. dataface/core/compile/models/chart/authored.py +25 -31
  18. dataface/core/compile/models/config.py +4 -0
  19. dataface/core/compile/models/style/theme.py +4 -3
  20. dataface/core/compile/sizing.py +41 -12
  21. dataface/core/compile/typography.py +23 -29
  22. dataface/core/compile/yaml_error_formatter.py +102 -33
  23. dataface/core/defaults/default_config.yml +1 -0
  24. dataface/core/defaults/themes/_base.yaml +6 -6
  25. dataface/core/defaults/themes/cream.yaml +9 -0
  26. dataface/core/defaults/themes/stark.yaml +11 -6
  27. dataface/core/execute/adapters/adapter_registry.py +3 -8
  28. dataface/core/execute/duckdb_cache.py +4 -3
  29. dataface/core/fonts.py +13 -0
  30. dataface/core/project.py +4 -0
  31. dataface/core/project_roots.py +15 -35
  32. dataface/core/render/board_links.py +71 -18
  33. dataface/core/render/chart/geo.py +74 -11
  34. dataface/core/render/chart/profile.py +65 -17
  35. dataface/core/render/chart/table.py +4 -32
  36. dataface/core/render/chart/time_unit_detect.py +28 -19
  37. dataface/core/render/dir_context.py +5 -3
  38. dataface/core/render/faces.py +25 -37
  39. dataface/core/render/font_measurement.py +2 -15
  40. dataface/core/render/layout_sizing.py +3 -0
  41. dataface/core/render/svg_utils.py +16 -7
  42. dataface/core/render/templates/nav/nav-fragment.html +1 -1
  43. dataface/core/render/templates/nav/nav.js +3 -3
  44. dataface/core/scoped_paths.py +30 -9
  45. dataface/core/serve/alias_index.py +24 -24
  46. dataface/core/serve/server.py +91 -61
  47. {dataface-0.1.6.dev76.dist-info → dataface-0.1.6.dev129.dist-info}/METADATA +1 -1
  48. {dataface-0.1.6.dev76.dist-info → dataface-0.1.6.dev129.dist-info}/RECORD +53 -53
  49. mdsvg/renderer.py +68 -25
  50. mdsvg/style.py +2 -2
  51. {dataface-0.1.6.dev76.dist-info → dataface-0.1.6.dev129.dist-info}/WHEEL +0 -0
  52. {dataface-0.1.6.dev76.dist-info → dataface-0.1.6.dev129.dist-info}/entry_points.txt +0 -0
  53. {dataface-0.1.6.dev76.dist-info → dataface-0.1.6.dev129.dist-info}/licenses/LICENSE +0 -0
@@ -6,9 +6,8 @@ from dataclasses import dataclass
6
6
  from pathlib import Path
7
7
 
8
8
  from dataface.core.project_roots import (
9
- discover_render_context as discover_render_context,
10
- discovery_boundary_for_face as discovery_boundary_for_face,
11
9
  find_dft_root as find_dft_root,
10
+ find_project_root as find_project_root,
12
11
  )
13
12
  from dataface.core.scoped_paths import (
14
13
  resolve_scoped_path as resolve_scoped_path,
@@ -60,8 +59,8 @@ class FaceRenderContext:
60
59
  """Path resolution result from a face path + project root.
61
60
 
62
61
  Adapter registry is built by `ProjectSession.open`, not by the context — call sites
63
- open a `ProjectSession` with `project_root` + `dbt_project_path` and read the
64
- registry off `project.adapter_registry`.
62
+ open a `ProjectSession` with `project_root` and read the registry off
63
+ `project.adapter_registry`.
65
64
  """
66
65
 
67
66
  face_file: Path
@@ -69,18 +68,16 @@ class FaceRenderContext:
69
68
  scoped_base: Path
70
69
  project_root: Path
71
70
  output_dir: Path
72
- dbt_project_path: Path | None = None
73
71
 
74
72
 
75
73
  def build_face_render_context(
76
74
  face_path: Path,
77
75
  project_dir: Path,
78
76
  ) -> FaceRenderContext:
79
- """Resolve a face path and walk for dbt context.
77
+ """Resolve a face path against the given project root.
80
78
 
81
- ``project_dir`` is authoritative; the walk only contributes the dbt project
82
- path. Callers must resolve their project dir first (e.g. via
83
- ``resolve_project_dir(raw_dir)`` at the CLI boundary).
79
+ ``project_dir`` is authoritative. Callers must resolve their project dir first
80
+ (e.g. via ``resolve_project_dir(raw_dir)`` at the CLI boundary).
84
81
  """
85
82
  if face_path.is_absolute():
86
83
  face_file = face_path.resolve()
@@ -89,10 +86,6 @@ def build_face_render_context(
89
86
  else:
90
87
  face_file = (project_dir / face_path).resolve()
91
88
 
92
- _, dbt_project_path = discover_render_context(
93
- face_file.parent,
94
- discovery_boundary_for_face(face_file.parent, project_dir),
95
- )
96
89
  project_root = project_dir
97
90
 
98
91
  try:
@@ -109,7 +102,6 @@ def build_face_render_context(
109
102
  scoped_base=project_root,
110
103
  project_root=project_root,
111
104
  output_dir=project_root,
112
- dbt_project_path=dbt_project_path,
113
105
  )
114
106
 
115
107
 
@@ -122,21 +114,18 @@ class YamlRenderContext:
122
114
 
123
115
  project_root: Path
124
116
  output_dir: Path
125
- dbt_project_path: Path | None = None
126
117
 
127
118
 
128
119
  def build_yaml_render_context(
129
120
  project_dir: Path,
130
121
  ) -> YamlRenderContext:
131
- """Walk for dbt context anchored at the given project root.
122
+ """Resolve the project root for rendering inline YAML.
132
123
 
133
124
  ``project_dir`` is authoritative. Callers must resolve their project dir
134
125
  first (e.g. via ``resolve_project_dir(raw_dir)`` at the CLI boundary).
135
126
  """
136
127
  project_root = project_dir.resolve()
137
- _, dbt_project_path = discover_render_context(project_root, None)
138
128
  return YamlRenderContext(
139
129
  project_root=project_root,
140
130
  output_dir=project_root,
141
- dbt_project_path=dbt_project_path,
142
131
  )
@@ -25,6 +25,7 @@ from dataface.core.registered_views.data_urls import (
25
25
 
26
26
  if TYPE_CHECKING:
27
27
  from dataface.agent_api.schema import SchemaResponse
28
+ from dataface.core.project import Project
28
29
  from dataface.core.serve.alias_index import AliasIndex
29
30
 
30
31
 
@@ -186,22 +187,15 @@ def data_paths_list(
186
187
  return result
187
188
 
188
189
 
189
- def build_alias_index_for_project(project_dir: Path) -> AliasIndex:
190
- """Build an AliasIndex from a project directory.
190
+ def build_alias_index_for_project(project: Project) -> AliasIndex:
191
+ """Build an AliasIndex from a project.
191
192
 
192
- Uses the same faces_at_root detection logic as the server. Intended for
193
- CLI commands (dft schema --data-paths) that need alias lookup without
194
- starting a server.
193
+ Intended for CLI commands (dft schema --data-paths) that need alias lookup
194
+ without starting a server.
195
195
  """
196
196
  from dataface.core.serve.alias_index import AliasIndex
197
197
 
198
- faces_dir = project_dir / "faces"
199
- faces_at_root = faces_dir.is_dir()
200
- return AliasIndex.build(
201
- project_dir=project_dir,
202
- faces_dir=faces_dir,
203
- faces_at_root=faces_at_root,
204
- )
198
+ return AliasIndex.build(project, faces_at_root=project.faces_dir.is_dir())
205
199
 
206
200
 
207
201
  def data_alias_errors_for_file(
@@ -128,8 +128,8 @@ Authored patch for bar and histogram charts.
128
128
  | `sort` | [ChartSort](#chartsort) | ✓ | Sort configuration: field to sort by and direction (asc/desc). |
129
129
  | `labels` | [ChartLabels](#chartlabels) | ✓ | Per-row text annotations near each data anchor. |
130
130
  | `data_table` | [ChartDataTable](#chartdatatable) | ✓ | Optional mini data-grid attached below/above the chart. |
131
- | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
132
- | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
131
+ | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
132
+ | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
133
133
  | `style` | [BarChartStyle](#barchartstyle) | ✓ | Chart-local style overrides. |
134
134
 
135
135
  <a id="linechart"></a>
@@ -156,8 +156,8 @@ Authored patch for line charts.
156
156
  | `sort` | [ChartSort](#chartsort) | ✓ | Sort configuration: field to sort by and direction (asc/desc). |
157
157
  | `labels` | [ChartLabels](#chartlabels) | ✓ | Per-row text annotations near each data anchor. |
158
158
  | `data_table` | [ChartDataTable](#chartdatatable) | ✓ | Optional mini data-grid attached below/above the chart. |
159
- | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
160
- | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
159
+ | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
160
+ | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
161
161
  | `style` | [LineChartStyle](#linechartstyle) | ✓ | Chart-local style overrides. |
162
162
 
163
163
  <a id="areachart"></a>
@@ -184,8 +184,8 @@ Authored patch for area charts.
184
184
  | `sort` | [ChartSort](#chartsort) | ✓ | Sort configuration: field to sort by and direction (asc/desc). |
185
185
  | `labels` | [ChartLabels](#chartlabels) | ✓ | Per-row text annotations near each data anchor. |
186
186
  | `data_table` | [ChartDataTable](#chartdatatable) | ✓ | Optional mini data-grid attached below/above the chart. |
187
- | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
188
- | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
187
+ | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
188
+ | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
189
189
  | `style` | [AreaChartStyle](#areachartstyle) | ✓ | Chart-local style overrides. |
190
190
 
191
191
  <a id="scatterchart"></a>
@@ -212,8 +212,8 @@ Authored patch for scatter charts.
212
212
  | `sort` | [ChartSort](#chartsort) | ✓ | Sort configuration: field to sort by and direction (asc/desc). |
213
213
  | `labels` | [ChartLabels](#chartlabels) | ✓ | Per-row text annotations near each data anchor. |
214
214
  | `data_table` | [ChartDataTable](#chartdatatable) | ✓ | Optional mini data-grid attached below/above the chart. |
215
- | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
216
- | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
215
+ | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
216
+ | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
217
217
  | `size` | str | ✓ | Field used to size-encode data points (quantitative). |
218
218
  | `shape` | str | ✓ | Field used to shape-encode data points (categorical). |
219
219
  | `style` | [ScatterChartStyle](#scatterchartstyle) | ✓ | Chart-local style overrides. |
@@ -242,8 +242,8 @@ Authored patch for heatmap charts.
242
242
  | `sort` | [ChartSort](#chartsort) | ✓ | Sort configuration: field to sort by and direction (asc/desc). |
243
243
  | `labels` | [ChartLabels](#chartlabels) | ✓ | Per-row text annotations near each data anchor. |
244
244
  | `data_table` | [ChartDataTable](#chartdatatable) | ✓ | Optional mini data-grid attached below/above the chart. |
245
- | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
246
- | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Not valid on kpi, table, callout, or spark_bar those renderers own their own sizing contracts. |
245
+ | `height` | int \| float | ✓ | Explicit chart height in pixels. Positive number only. When set, overrides aspect_ratio and theme cascade. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
246
+ | `width` | int \| float | ✓ | Chart width hint in pixels. Positive number only. Used by the label-overlap heuristic to determine whether x-axis labels need tilting. Does not override the layout slot width — the chart still fills its allocated container. Only valid on cartesian chart families: area, bar, heatmap, histogram, line, and scatter. Other chart families use renderer-owned or layout-owned sizing contracts. |
247
247
  | `style` | [HeatmapChartStyle](#heatmapchartstyle) | ✓ | Chart-local style overrides. |
248
248
 
249
249
  <a id="piechart"></a>
@@ -1081,7 +1081,7 @@ Authored overlay for TitleStyle. Board and face titles.
1081
1081
  | Field | Type | Optional | Description |
1082
1082
  |-------|------|:--------:|-------------|
1083
1083
  | `font` | [FontStyle](#fontstyle) | ✓ | Title font style overrides. |
1084
- | `line_height` | float | ✓ | Line height multiplier for titles and markdown headings. Headings typically want a tighter multiplier than body prose (~1.1-1.25 vs the body 1.5-1.6). |
1084
+ | `line_height` | float | ✓ | Line height multiplier for titles and markdown headings. Headings typically want a tighter multiplier than body prose. |
1085
1085
  | `sizes` | list[float] | ✓ | Font sizes for the H1–H6 heading ramp, indexed by ``face.level - 1``. Combined with ``width_offsets`` at render time to size titles responsively by card width. |
1086
1086
  | `width_offsets` | [TitleWidthOffsetsStyle](#titlewidthoffsetsstyle) | ✓ | Additive level offsets by card width (tiny/narrow/medium/wide). Added to the title's base level before indexing ``sizes``. Consumed by chart_title_spec / face_title_spec. |
1087
1087
  | `min_height` | float | ✓ | Minimum title row height in pixels. |
@@ -2372,10 +2372,11 @@ Authored overlay for SparkAreaStyle.
2372
2372
 
2373
2373
  <a id="geoshapemarkstyle"></a>
2374
2374
  ## GeoshapeMarkStyle
2375
- Authored overlay for GeoshapeMarkStyle. Geoshape (choropleth) mark boundary stroke.
2375
+ Authored overlay for GeoshapeMarkStyle. Geoshape (choropleth) mark fill and boundary stroke.
2376
2376
 
2377
2377
  | Field | Type | Optional | Description |
2378
2378
  |-------|------|:--------:|-------------|
2379
+ | `fill` | str | ✓ | Neutral geoshape fill color. |
2379
2380
  | `stroke` | [StrokeStyle](#strokestyle) | ✓ | Geoshape boundary stroke style. |
2380
2381
 
2381
2382
  <a id="circlemarkstyle"></a>
@@ -16,6 +16,8 @@ from pathlib import Path
16
16
 
17
17
  from pydantic import BaseModel, Field
18
18
 
19
+ from dataface.core.project import Project
20
+
19
21
  # Cap grep/glob payloads — read_file returns full content (model must handle size).
20
22
  _MAX_GREP_MATCHES = 200
21
23
  _MAX_GLOB_MATCHES = 500
@@ -146,9 +148,9 @@ def _walk_project(root: Path) -> list[Path]:
146
148
  return result
147
149
 
148
150
 
149
- def read_file(path: str, project_dir: Path) -> ReadFileResult:
151
+ def read_file(path: str, project: Project) -> ReadFileResult:
150
152
  try:
151
- target = _resolve_within(project_dir, path)
153
+ target = _resolve_within(project.root, path)
152
154
  except ValueError as exc:
153
155
  return ReadFileResult(success=False, path=path, error=str(exc))
154
156
  if not target.is_file():
@@ -157,12 +159,14 @@ def read_file(path: str, project_dir: Path) -> ReadFileResult:
157
159
  content = target.read_text(encoding="utf-8")
158
160
  except (OSError, UnicodeDecodeError) as exc:
159
161
  return ReadFileResult(success=False, path=path, error=str(exc))
160
- return ReadFileResult(success=True, path=_rel(project_dir, target), content=content)
162
+ return ReadFileResult(
163
+ success=True, path=_rel(project.root, target), content=content
164
+ )
161
165
 
162
166
 
163
- def write_file(path: str, content: str, project_dir: Path) -> WriteFileResult:
167
+ def write_file(path: str, content: str, project: Project) -> WriteFileResult:
164
168
  try:
165
- target = _resolve_within(project_dir, path)
169
+ target = _resolve_within(project.root, path)
166
170
  except ValueError as exc:
167
171
  return WriteFileResult(success=False, path=path, error=str(exc))
168
172
  try:
@@ -172,16 +176,16 @@ def write_file(path: str, content: str, project_dir: Path) -> WriteFileResult:
172
176
  return WriteFileResult(success=False, path=path, error=str(exc))
173
177
  return WriteFileResult(
174
178
  success=True,
175
- path=_rel(project_dir, target),
179
+ path=_rel(project.root, target),
176
180
  bytes_written=len(content.encode("utf-8")),
177
181
  )
178
182
 
179
183
 
180
184
  def edit_file(
181
- path: str, old_string: str, new_string: str, project_dir: Path
185
+ path: str, old_string: str, new_string: str, project: Project
182
186
  ) -> EditFileResult:
183
187
  try:
184
- target = _resolve_within(project_dir, path)
188
+ target = _resolve_within(project.root, path)
185
189
  except ValueError as exc:
186
190
  return EditFileResult(success=False, path=path, error=str(exc))
187
191
  if not target.is_file():
@@ -205,11 +209,11 @@ def edit_file(
205
209
  target.write_text(original.replace(old_string, new_string), encoding="utf-8")
206
210
  except OSError as exc:
207
211
  return EditFileResult(success=False, path=path, error=str(exc))
208
- return EditFileResult(success=True, path=_rel(project_dir, target), replacements=1)
212
+ return EditFileResult(success=True, path=_rel(project.root, target), replacements=1)
209
213
 
210
214
 
211
- def glob_files(pattern: str, project_dir: Path) -> GlobResult:
212
- root = project_dir.resolve()
215
+ def glob_files(pattern: str, project: Project) -> GlobResult:
216
+ root = project.root.resolve()
213
217
  matches: list[str] = []
214
218
  try:
215
219
  candidates = root.glob(pattern)
@@ -228,8 +232,8 @@ def glob_files(pattern: str, project_dir: Path) -> GlobResult:
228
232
  return GlobResult(success=True, matches=matches)
229
233
 
230
234
 
231
- def grep_files(pattern: str, project_dir: Path, glob: str | None = None) -> GrepResult:
232
- root = project_dir.resolve()
235
+ def grep_files(pattern: str, project: Project, glob: str | None = None) -> GrepResult:
236
+ root = project.root.resolve()
233
237
  matches: list[GrepMatch] = []
234
238
  try:
235
239
  candidates: list[Path] = (
@@ -446,8 +446,8 @@ def apply_proposal(
446
446
  )
447
447
 
448
448
  # Ensure base dirs exist
449
- (project.root / "faces").mkdir(exist_ok=True)
450
- (project.root / "faces" / "partials").mkdir(exist_ok=True)
449
+ project.faces_dir.mkdir(exist_ok=True)
450
+ (project.faces_dir / "partials").mkdir(exist_ok=True)
451
451
 
452
452
  # Write partials — skipped by validate_paths (_*.yml convention)
453
453
  for partial_filename, partial_path, rel in partial_targets:
@@ -17,7 +17,6 @@ else:
17
17
  # Verb-forwarder imports use SUBMODULES so test monkeypatches on the
18
18
  # module attribute affect the call site.
19
19
  from dataface.agent_api import (
20
- dashboards as _dashboards,
21
20
  describe as _describe,
22
21
  describe_query as _describe_query,
23
22
  query as _query,
@@ -42,7 +41,7 @@ from dataface.core.inspect.query_validator import (
42
41
  validate_query as _core_validate_query,
43
42
  )
44
43
  from dataface.core.project import Project
45
- from dataface.core.project_roots import discover_render_context
44
+ from dataface.core.project_roots import find_project_root
46
45
 
47
46
  if TYPE_CHECKING:
48
47
  from dataface.core.dashboard import RenderedDashboard, RenderFormat
@@ -71,7 +70,6 @@ class ProjectSession:
71
70
  cache: DuckDBCache | None
72
71
  _owns_registry: bool
73
72
  _read_only: bool
74
- _dbt_project_path: Path | None
75
73
  _connection_string: str | None
76
74
  _dialect: str
77
75
  _target: str
@@ -96,7 +94,6 @@ class ProjectSession:
96
94
  # short-circuit the build on first access.
97
95
  self.__dict__["adapter_registry"] = adapter_registry
98
96
  self._read_only = read_only
99
- self._dbt_project_path = None
100
97
  self._connection_string = None
101
98
  self._dialect = "duckdb"
102
99
  self._target = "dev"
@@ -111,7 +108,6 @@ class ProjectSession:
111
108
  *,
112
109
  cache: DuckDBCache | None = None,
113
110
  read_only: bool = True,
114
- dbt_project_path: Path | None = None,
115
111
  connection_string: str | None = None,
116
112
  dialect: str = "duckdb",
117
113
  target: str = "dev",
@@ -135,7 +131,6 @@ class ProjectSession:
135
131
  cache=cache,
136
132
  read_only=read_only,
137
133
  )
138
- session._dbt_project_path = dbt_project_path
139
134
  session._connection_string = connection_string
140
135
  session._dialect = dialect
141
136
  session._target = target
@@ -151,7 +146,6 @@ class ProjectSession:
151
146
  *,
152
147
  cache: DuckDBCache | None = None,
153
148
  read_only: bool = True,
154
- dbt_project_path: Path | None = None,
155
149
  connection_string: str | None = None,
156
150
  dialect: str = "duckdb",
157
151
  target: str = "dev",
@@ -166,7 +160,6 @@ class ProjectSession:
166
160
  this method stores it directly without re-wrapping.
167
161
  """
168
162
  session = cls(project=project, cache=cache, read_only=read_only)
169
- session._dbt_project_path = dbt_project_path
170
163
  session._connection_string = connection_string
171
164
  session._dialect = dialect
172
165
  session._target = target
@@ -185,13 +178,8 @@ class ProjectSession:
185
178
  ) -> Self:
186
179
  """Open a ProjectSession rooted at the project directory discovered upward from face_file."""
187
180
  resolved = Path(face_file).resolve()
188
- project_root, dbt_project_path = discover_render_context(resolved.parent, None)
189
- return cls.open(
190
- project_root,
191
- read_only=read_only,
192
- cache=cache,
193
- dbt_project_path=dbt_project_path,
194
- )
181
+ project_root = find_project_root(resolved.parent, boundary=None)
182
+ return cls.open(project_root, read_only=read_only, cache=cache)
195
183
 
196
184
  def __enter__(self) -> Self:
197
185
  return self
@@ -210,7 +198,6 @@ class ProjectSession:
210
198
  return build_adapter_registry(
211
199
  self.project,
212
200
  read_only=self._read_only,
213
- dbt_project_path=self._dbt_project_path,
214
201
  connection_string=self._connection_string,
215
202
  profile_type=self._dialect,
216
203
  target=self._target,
@@ -253,6 +240,10 @@ class ProjectSession:
253
240
  def warnings_ignore(self) -> frozenset[str]:
254
241
  return self.project.warnings_ignore
255
242
 
243
+ @property
244
+ def faces_dir(self) -> Path:
245
+ return self.project.faces_dir
246
+
256
247
  @cached_property
257
248
  def _relationship_context(self) -> RelationshipContext | None:
258
249
  """Load relationship context from the super-schema cache (lazy, per-instance).
@@ -275,11 +266,6 @@ class ProjectSession:
275
266
 
276
267
  # ── Verb forwarders ──────────────────────────────────────────────────────
277
268
 
278
- def validate(self, face_path: Path) -> ValidateResult:
279
- result = _validate.validate(face_path, project=self.project)
280
- annotated = _validate.annotate_with_data_lint([result], project_session=self)
281
- return annotated[0]
282
-
283
269
  def validate_paths(self, paths: list[Path] | None) -> list[ValidateResult]:
284
270
  results = _validate.validate_paths(paths, project=self.project)
285
271
  return _validate.annotate_with_data_lint(results, project_session=self)
@@ -342,32 +328,9 @@ class ProjectSession:
342
328
  relationship_context=self._relationship_context,
343
329
  )
344
330
 
345
- def describe_face(self, path: Path) -> _describe.DescribeFaceResult:
346
- return _describe.describe_face(path, project=self.project)
347
-
348
331
  def describe_paths(self, paths: list[Path]) -> list[_describe.DescribeFaceResult]:
349
332
  return _describe.describe_paths(paths, project=self.project)
350
333
 
351
- def list_dashboards(
352
- self,
353
- directory: Path | None = None,
354
- recursive: bool = True,
355
- ) -> _dashboards.ListDashboardsResult:
356
- """When ``directory`` is None, defaults to ``self.project.root``. When provided, callers are responsible for passing an absolute path; relative paths resolve against the process working directory."""
357
- return _dashboards.list_dashboards(
358
- directory=directory if directory is not None else self.project.root,
359
- recursive=recursive,
360
- )
361
-
362
- def get_dashboard(
363
- self,
364
- path: Path,
365
- include_raw: bool = False,
366
- ) -> _dashboards.CompiledDashboard:
367
- return _dashboards.get_dashboard(
368
- path, include_raw=include_raw, project=self.project
369
- )
370
-
371
334
  def lookup_face_query_sql(
372
335
  self,
373
336
  name: str,
@@ -15,10 +15,6 @@ from dataface.core.compile.markdown import (
15
15
  )
16
16
  from dataface.core.execute.duckdb_cache import DuckDBCache
17
17
  from dataface.core.project import Project
18
- from dataface.core.project_roots import (
19
- discover_render_context,
20
- discovery_boundary_for_face,
21
- )
22
18
  from dataface.core.render.face_api import compile_and_render
23
19
 
24
20
 
@@ -72,16 +68,7 @@ def render_face(
72
68
  if project is not None:
73
69
  if warnings_ignore is None:
74
70
  warnings_ignore = project.warnings_ignore
75
- # Discover dbt_project.yml between the face and the project root.
76
- # build_adapter_registry's own fallback would only walk upward from
77
- # project.root, missing nested dbt projects under it.
78
- _, dbt_project_path = discover_render_context(
79
- face_file.parent,
80
- discovery_boundary_for_face(face_file.parent, project.root),
81
- )
82
- project_session = ProjectSession.open(
83
- project.root, cache=cache, dbt_project_path=dbt_project_path
84
- )
71
+ project_session = ProjectSession.open(project.root, cache=cache)
85
72
  else:
86
73
  if warnings_ignore is None:
87
74
  warnings_ignore = frozenset()
@@ -90,7 +90,7 @@ def _validate_one_path(
90
90
  # detectors. Keep this lazy so `dft --help` doesn't pay that startup cost.
91
91
  from dataface.core.inspect.manifest_utils import INSPECT_TEMPLATE_MANIFEST
92
92
 
93
- raw_path = path if path is not None else project.root / "faces"
93
+ raw_path = path if path is not None else project.faces_dir
94
94
 
95
95
  try:
96
96
  resolved = resolve_scoped_path(raw_path, project.root)
dataface/ai/context.py CHANGED
@@ -21,6 +21,7 @@ class DatafaceAIContext:
21
21
  # `dft chat` after binding; consumed by `_view_url` to build clickable
22
22
  # localhost URLs in `render_dashboard` responses (including as_link=True).
23
23
  server_port: int | None = None
24
+ prompt_context: dict[str, str] | None = None
24
25
 
25
26
  def resolve_dashboard_path(self, path: Path) -> Path:
26
27
  """Resolve a dashboard path inside the scoped dashboards directory.
@@ -31,7 +32,7 @@ class DatafaceAIContext:
31
32
  if path.is_absolute():
32
33
  raise ValueError(f"Dashboard path must be relative: {path}")
33
34
  if self.dashboards_directory is None:
34
- base_dir = (self.project_session.project.root / "faces").resolve()
35
+ base_dir = self.project_session.faces_dir.resolve()
35
36
  else:
36
37
  base_dir = self.dashboards_directory.resolve()
37
38
  resolved = (base_dir / path).resolve()
@@ -293,14 +293,14 @@ def _handle_get_warning_code(
293
293
  def _handle_read_file(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
294
294
  parsed = _files.ReadFileArgs.model_validate(args)
295
295
  return _files.read_file(
296
- parsed.path, project_dir=_render_project_dir(args, ctx)
296
+ parsed.path, project=Project(_render_project_dir(args, ctx))
297
297
  ).model_dump(mode="json", exclude_none=True)
298
298
 
299
299
 
300
300
  def _handle_write_file(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
301
301
  parsed = _files.WriteFileArgs.model_validate(args)
302
302
  return _files.write_file(
303
- parsed.path, parsed.content, project_dir=_render_project_dir(args, ctx)
303
+ parsed.path, parsed.content, project=Project(_render_project_dir(args, ctx))
304
304
  ).model_dump(mode="json", exclude_none=True)
305
305
 
306
306
 
@@ -310,14 +310,14 @@ def _handle_edit_file(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str,
310
310
  parsed.path,
311
311
  parsed.old_string,
312
312
  parsed.new_string,
313
- project_dir=_render_project_dir(args, ctx),
313
+ project=Project(_render_project_dir(args, ctx)),
314
314
  ).model_dump(mode="json", exclude_none=True)
315
315
 
316
316
 
317
317
  def _handle_glob_files(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str, Any]:
318
318
  parsed = _files.GlobFilesArgs.model_validate(args)
319
319
  return _files.glob_files(
320
- parsed.pattern, project_dir=_render_project_dir(args, ctx)
320
+ parsed.pattern, project=Project(_render_project_dir(args, ctx))
321
321
  ).model_dump(mode="json", exclude_none=True)
322
322
 
323
323
 
@@ -325,7 +325,7 @@ def _handle_grep_files(args: dict[str, Any], ctx: DatafaceAIContext) -> dict[str
325
325
  parsed = _files.GrepFilesArgs.model_validate(args)
326
326
  return _files.grep_files(
327
327
  parsed.pattern,
328
- project_dir=_render_project_dir(args, ctx),
328
+ project=Project(_render_project_dir(args, ctx)),
329
329
  glob=parsed.glob,
330
330
  ).model_dump(mode="json", exclude_none=True)
331
331
 
@@ -129,11 +129,7 @@ def render_command(
129
129
 
130
130
  with (
131
131
  cache_from_env() as cache,
132
- ProjectSession.open(
133
- ctx.project_root,
134
- cache=cache,
135
- dbt_project_path=ctx.dbt_project_path,
136
- ) as project_session,
132
+ ProjectSession.open(ctx.project_root, cache=cache) as project_session,
137
133
  ):
138
134
  result = project_session.render_dashboard(
139
135
  path=ctx.scoped_path,
@@ -219,11 +215,7 @@ def render_command_from_yaml(
219
215
 
220
216
  with (
221
217
  cache_from_env() as cache,
222
- ProjectSession.open(
223
- ctx.project_root,
224
- cache=cache,
225
- dbt_project_path=ctx.dbt_project_path,
226
- ) as project_session,
218
+ ProjectSession.open(ctx.project_root, cache=cache) as project_session,
227
219
  ):
228
220
  result = project_session.render_dashboard(
229
221
  yaml_content=yaml_content,
@@ -193,32 +193,31 @@ def schema_command(
193
193
  lineage_depth=lineage_depth,
194
194
  surface="cli",
195
195
  )
196
-
197
- if data_paths:
198
- if not drill_result.success:
199
- print_structured_errors(
200
- drill_result.structured_errors
201
- or [
202
- StructuredError(
203
- code=DF_UNKNOWN_INTERNAL.code,
204
- domain=DF_UNKNOWN_INTERNAL.domain,
205
- doc_url=DF_UNKNOWN_INTERNAL.doc_url,
206
- docs_topic=DF_UNKNOWN_INTERNAL.docs_topic,
207
- message="; ".join(drill_result.errors),
208
- )
209
- ]
196
+ if data_paths:
197
+ if not drill_result.success:
198
+ print_structured_errors(
199
+ drill_result.structured_errors
200
+ or [
201
+ StructuredError(
202
+ code=DF_UNKNOWN_INTERNAL.code,
203
+ domain=DF_UNKNOWN_INTERNAL.domain,
204
+ doc_url=DF_UNKNOWN_INTERNAL.doc_url,
205
+ docs_topic=DF_UNKNOWN_INTERNAL.docs_topic,
206
+ message="; ".join(drill_result.errors),
207
+ )
208
+ ]
209
+ )
210
+ raise typer.Exit(1)
211
+ from dataface.agent_api.data_paths import (
212
+ build_alias_index_for_project,
213
+ data_paths_list,
210
214
  )
211
- raise typer.Exit(1)
212
- from dataface.agent_api.data_paths import (
213
- build_alias_index_for_project,
214
- data_paths_list,
215
- )
216
215
 
217
- alias_index = build_alias_index_for_project(project_root)
218
- ep_list = data_paths_list(drill_result, alias_index=alias_index)
219
- wire = [p.to_dict() for p in ep_list]
220
- typer.echo(json.dumps(wire, indent=2))
221
- return
216
+ alias_index = build_alias_index_for_project(project_session.project)
217
+ ep_list = data_paths_list(drill_result, alias_index=alias_index)
218
+ wire = [p.to_dict() for p in ep_list]
219
+ typer.echo(json.dumps(wire, indent=2))
220
+ return
222
221
 
223
222
  if json_output:
224
223
  typer.echo(