dataface 0.1.6.dev62__py3-none-any.whl → 0.1.6.dev82__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 (33) hide show
  1. dataface/agent_api/_paths.py +19 -25
  2. dataface/agent_api/data_paths.py +6 -12
  3. dataface/agent_api/docs/yaml-reference.md +1 -1
  4. dataface/agent_api/files.py +17 -13
  5. dataface/agent_api/pack.py +2 -2
  6. dataface/agent_api/project_session.py +4 -0
  7. dataface/agent_api/validate.py +1 -1
  8. dataface/ai/context.py +1 -1
  9. dataface/ai/llm.py +36 -5
  10. dataface/ai/tools/__init__.py +5 -5
  11. dataface/cli/commands/render.py +8 -2
  12. dataface/cli/commands/schema.py +23 -24
  13. dataface/core/compile/models/style/theme.py +1 -2
  14. dataface/core/compile/sizing.py +41 -12
  15. dataface/core/compile/typography.py +23 -29
  16. dataface/core/defaults/themes/_base.yaml +6 -6
  17. dataface/core/defaults/themes/cream.yaml +9 -0
  18. dataface/core/defaults/themes/stark.yaml +7 -6
  19. dataface/core/fonts.py +13 -0
  20. dataface/core/project.py +4 -0
  21. dataface/core/render/faces.py +24 -35
  22. dataface/core/render/font_measurement.py +2 -15
  23. dataface/core/render/layout_sizing.py +3 -0
  24. dataface/core/render/svg_utils.py +16 -7
  25. dataface/core/serve/alias_index.py +15 -7
  26. dataface/core/serve/server.py +9 -24
  27. {dataface-0.1.6.dev62.dist-info → dataface-0.1.6.dev82.dist-info}/METADATA +1 -1
  28. {dataface-0.1.6.dev62.dist-info → dataface-0.1.6.dev82.dist-info}/RECORD +33 -33
  29. mdsvg/renderer.py +68 -25
  30. mdsvg/style.py +2 -2
  31. {dataface-0.1.6.dev62.dist-info → dataface-0.1.6.dev82.dist-info}/WHEEL +0 -0
  32. {dataface-0.1.6.dev62.dist-info → dataface-0.1.6.dev82.dist-info}/entry_points.txt +0 -0
  33. {dataface-0.1.6.dev62.dist-info → dataface-0.1.6.dev82.dist-info}/licenses/LICENSE +0 -0
@@ -65,7 +65,7 @@ class FaceRenderContext:
65
65
  """
66
66
 
67
67
  face_file: Path
68
- scoped_path: Path | None
68
+ scoped_path: Path
69
69
  scoped_base: Path
70
70
  project_root: Path
71
71
  output_dir: Path
@@ -74,37 +74,34 @@ class FaceRenderContext:
74
74
 
75
75
  def build_face_render_context(
76
76
  face_path: Path,
77
- project_dir: Path | None = None,
77
+ project_dir: Path,
78
78
  ) -> FaceRenderContext:
79
79
  """Resolve a face path and walk for dbt context.
80
80
 
81
- ``project_dir=None`` means "walk freely from the face's parent" — used by
82
- the CLI when ``--project-dir`` is omitted so a face under a dbt sub-project
83
- still anchors on that sub-project's root. A given ``project_dir`` is
84
- authoritative; the walk only contributes the dbt project path.
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).
85
84
  """
86
85
  if face_path.is_absolute():
87
86
  face_file = face_path.resolve()
88
87
  elif ".." in face_path.parts:
89
88
  face_file = (Path.cwd() / face_path).resolve()
90
89
  else:
91
- anchor = (
92
- project_dir.resolve()
93
- if project_dir is not None
94
- else resolve_project_dir(None)
95
- )
96
- face_file = (anchor / face_path).resolve()
97
-
98
- walk_root, dbt_project_path = discover_render_context(
90
+ face_file = (project_dir / face_path).resolve()
91
+
92
+ _, dbt_project_path = discover_render_context(
99
93
  face_file.parent,
100
94
  discovery_boundary_for_face(face_file.parent, project_dir),
101
95
  )
102
- project_root = project_dir.resolve() if project_dir is not None else walk_root
96
+ project_root = project_dir
103
97
 
104
98
  try:
105
- scoped_path: Path | None = face_file.relative_to(project_root)
99
+ scoped_path: Path = face_file.relative_to(project_root)
106
100
  except ValueError:
107
- scoped_path = face_file
101
+ raise ValueError(
102
+ f"Face file {face_file} is outside project_dir {project_root}. "
103
+ f"Pass an explicit --project-dir that contains the face file."
104
+ ) from None
108
105
 
109
106
  return FaceRenderContext(
110
107
  face_file=face_file,
@@ -129,18 +126,15 @@ class YamlRenderContext:
129
126
 
130
127
 
131
128
  def build_yaml_render_context(
132
- project_dir: Path | None = None,
129
+ project_dir: Path,
133
130
  ) -> YamlRenderContext:
134
131
  """Walk for dbt context anchored at the given project root.
135
132
 
136
- ``project_dir=None`` walks from cwd to discover the project root; a given
137
- ``project_dir`` is authoritative.
133
+ ``project_dir`` is authoritative. Callers must resolve their project dir
134
+ first (e.g. via ``resolve_project_dir(raw_dir)`` at the CLI boundary).
138
135
  """
139
- anchor = (
140
- project_dir.resolve() if project_dir is not None else resolve_project_dir(None)
141
- )
142
- walk_root, dbt_project_path = discover_render_context(anchor, None)
143
- project_root = anchor if project_dir is not None else walk_root
136
+ project_root = project_dir.resolve()
137
+ _, dbt_project_path = discover_render_context(project_root, None)
144
138
  return YamlRenderContext(
145
139
  project_root=project_root,
146
140
  output_dir=project_root,
@@ -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(
@@ -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. |
@@ -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:
@@ -253,6 +253,10 @@ class ProjectSession:
253
253
  def warnings_ignore(self) -> frozenset[str]:
254
254
  return self.project.warnings_ignore
255
255
 
256
+ @property
257
+ def faces_dir(self) -> Path:
258
+ return self.project.faces_dir
259
+
256
260
  @cached_property
257
261
  def _relationship_context(self) -> RelationshipContext | None:
258
262
  """Load relationship context from the super-schema cache (lazy, per-instance).
@@ -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
@@ -31,7 +31,7 @@ class DatafaceAIContext:
31
31
  if path.is_absolute():
32
32
  raise ValueError(f"Dashboard path must be relative: {path}")
33
33
  if self.dashboards_directory is None:
34
- base_dir = (self.project_session.project.root / "faces").resolve()
34
+ base_dir = self.project_session.faces_dir.resolve()
35
35
  else:
36
36
  base_dir = self.dashboards_directory.resolve()
37
37
  resolved = (base_dir / path).resolve()
dataface/ai/llm.py CHANGED
@@ -50,7 +50,8 @@ def _resolve_model(
50
50
  def _normalize_openai_tools(
51
51
  tools: list[dict[str, Any]] | None,
52
52
  ) -> list[dict[str, Any]]:
53
- source_tools = tools or ALL_TOOLS
53
+ # None means "use default agent tools"; [] means "no tools, text-only completion".
54
+ source_tools = ALL_TOOLS if tools is None else tools
54
55
  if not source_tools:
55
56
  return []
56
57
  if "input_schema" in source_tools[0]:
@@ -69,7 +70,8 @@ def _normalize_openai_tools(
69
70
  def _normalize_anthropic_tools(
70
71
  tools: list[dict[str, Any]] | None,
71
72
  ) -> list[dict[str, Any]]:
72
- source_tools = tools or ALL_TOOLS
73
+ # None means "use default agent tools"; [] means "no tools, text-only completion".
74
+ source_tools = ALL_TOOLS if tools is None else tools
73
75
  if not source_tools:
74
76
  return []
75
77
  if "input_schema" in source_tools[0]:
@@ -120,8 +122,14 @@ def _is_anthropic_api_error(exc: Exception) -> bool:
120
122
  # -- Provider stream adapter ------------------------------------------------
121
123
 
122
124
 
123
- def _iter_anthropic_stream(stream_ctx: Any) -> Iterator[StreamEvent]:
124
- """Parse Anthropic Messages API stream context manager into typed events."""
125
+ def _iter_anthropic_stream(
126
+ stream_ctx: Any, usage_sink: Any = None
127
+ ) -> Iterator[StreamEvent]:
128
+ """Parse Anthropic Messages API stream context manager into typed events.
129
+
130
+ ``usage_sink``, when given, is called once with the final message's usage
131
+ payload so the client can accumulate token totals.
132
+ """
125
133
  with stream_ctx as stream:
126
134
  for event in stream:
127
135
  if event.type == "content_block_delta" and event.delta.type == "text_delta":
@@ -129,6 +137,9 @@ def _iter_anthropic_stream(stream_ctx: Any) -> Iterator[StreamEvent]:
129
137
 
130
138
  final_message = stream.get_final_message()
131
139
 
140
+ if usage_sink is not None:
141
+ usage_sink(final_message.usage)
142
+
132
143
  for block in final_message.content:
133
144
  if block.type == "tool_use":
134
145
  yield ToolCallEvent(
@@ -144,6 +155,13 @@ class LLMClient(Protocol):
144
155
 
145
156
  provider: str
146
157
  model: str
158
+ # Cumulative usage across this client's lifetime. Every implementation must
159
+ # maintain these honestly — eval solvers read them to attribute per-case
160
+ # cost/calls. Declared here so callers access them directly (no getattr
161
+ # defaults, which would silently fabricate zeros for a client that forgot them).
162
+ total_input_tokens: int
163
+ total_output_tokens: int
164
+ llm_calls: int
147
165
 
148
166
  def stream_with_tools(
149
167
  self,
@@ -332,6 +350,17 @@ class AnthropicClient:
332
350
  )
333
351
  self.api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
334
352
  self._client: Any = None
353
+ # Cumulative token usage + call count (see LLMClient protocol).
354
+ self.total_input_tokens = 0
355
+ self.total_output_tokens = 0
356
+ self.llm_calls = 0
357
+
358
+ def _accumulate_usage(self, usage: Any) -> None:
359
+ """Add an Anthropic Messages usage payload to the running token totals."""
360
+ if usage is None:
361
+ return
362
+ self.total_input_tokens += getattr(usage, "input_tokens", 0) or 0
363
+ self.total_output_tokens += getattr(usage, "output_tokens", 0) or 0
335
364
 
336
365
  @property
337
366
  def client(self) -> Any:
@@ -404,6 +433,7 @@ class AnthropicClient:
404
433
  system_prompt: str,
405
434
  tools: list[dict[str, Any]] | None = None,
406
435
  ) -> Iterator[StreamEvent]:
436
+ self.llm_calls += 1
407
437
  try:
408
438
  yield from _iter_anthropic_stream(
409
439
  self.client.messages.stream(
@@ -412,7 +442,8 @@ class AnthropicClient:
412
442
  messages=self._messages_to_input(messages),
413
443
  tools=_normalize_anthropic_tools(tools),
414
444
  max_tokens=4096,
415
- )
445
+ ),
446
+ usage_sink=self._accumulate_usage,
416
447
  )
417
448
  except Exception as exc:
418
449
  if _is_anthropic_api_error(exc):
@@ -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
 
@@ -8,6 +8,8 @@ from dataface.agent_api import ProjectSession
8
8
  from dataface.agent_api._paths import (
9
9
  build_face_render_context,
10
10
  build_yaml_render_context,
11
+ resolve_project_dir,
12
+ resolve_project_dir_from_paths,
11
13
  )
12
14
  from dataface.agent_api.cache import cache_from_env
13
15
  from dataface.agent_api.dashboards import RenderedDashboard
@@ -119,7 +121,10 @@ def render_command(
119
121
  if ignore_codes:
120
122
  _emit_unknown_code_notices(ignore_codes)
121
123
 
122
- ctx = build_face_render_context(face_path, project_dir)
124
+ resolved_project_dir = resolve_project_dir_from_paths(
125
+ [face_path.resolve()], project_dir
126
+ )
127
+ ctx = build_face_render_context(face_path, resolved_project_dir)
123
128
  output_dir = ctx.output_dir
124
129
 
125
130
  with (
@@ -208,7 +213,8 @@ def render_command_from_yaml(
208
213
  if ignore_codes:
209
214
  _emit_unknown_code_notices(ignore_codes)
210
215
 
211
- ctx = build_yaml_render_context(project_dir)
216
+ resolved_project_dir = resolve_project_dir(project_dir)
217
+ ctx = build_yaml_render_context(resolved_project_dir)
212
218
  output_dir = ctx.output_dir
213
219
 
214
220
  with (
@@ -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(
@@ -332,8 +332,7 @@ class TitleStyle(BaseModel):
332
332
  line_height: float = Field(
333
333
  description=(
334
334
  "Line height multiplier for titles and markdown headings. Headings "
335
- "typically want a tighter multiplier than body prose (~1.1-1.25 vs "
336
- "the body 1.5-1.6)."
335
+ "typically want a tighter multiplier than body prose."
337
336
  )
338
337
  )
339
338
  sizes: list[float] = Field(