dataface 0.1.5.dev88__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 (84) hide show
  1. dataface/DATAFACE_SYNTAX.md +7 -6
  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 +10 -5
  9. dataface/agent_api/docs/yaml-reference.md +232 -239
  10. dataface/agent_api/init.py +3 -36
  11. dataface/agent_api/inspect.py +4 -7
  12. dataface/agent_api/mcp_install.py +3 -3
  13. dataface/agent_api/project.py +224 -0
  14. dataface/agent_api/query.py +2 -1
  15. dataface/agent_api/surface_aliases.yaml +3 -3
  16. dataface/agent_api/validate.py +13 -8
  17. dataface/ai/mcp/server.py +4 -3
  18. dataface/ai/tools/__init__.py +16 -6
  19. dataface/cli/commands/describe.py +4 -1
  20. dataface/cli/commands/extension.py +21 -115
  21. dataface/cli/commands/init.py +13 -51
  22. dataface/cli/commands/inspect.py +2 -1
  23. dataface/cli/commands/mcp_init.py +23 -14
  24. dataface/cli/commands/query.py +54 -44
  25. dataface/cli/commands/render.py +18 -13
  26. dataface/cli/commands/serve.py +8 -9
  27. dataface/cli/commands/skills_init.py +3 -5
  28. dataface/cli/commands/validate.py +4 -1
  29. dataface/cli/main.py +47 -42
  30. dataface/core/compile/compiler.py +0 -1
  31. dataface/core/compile/config.py +9 -61
  32. dataface/core/compile/introspection.py +8 -0
  33. dataface/core/compile/meta.py +2 -25
  34. dataface/core/compile/models/chart/authored.py +18 -238
  35. dataface/core/compile/models/chart/resolved.py +2 -4
  36. dataface/core/compile/models/config.py +12 -9
  37. dataface/core/compile/models/palette.py +1 -1
  38. dataface/core/compile/models/query/normalized.py +30 -13
  39. dataface/core/compile/models/refs.py +16 -10
  40. dataface/core/compile/models/style/authored.py +218 -0
  41. dataface/core/compile/models/style/theme.py +4 -2
  42. dataface/core/compile/models/variable/authored.py +27 -11
  43. dataface/core/compile/models/vega_lite/config.py +2 -14
  44. dataface/core/compile/normalize_queries.py +4 -1
  45. dataface/core/compile/normalize_variables.py +19 -4
  46. dataface/core/compile/palette.py +2 -2
  47. dataface/core/compile/parser.py +0 -1
  48. dataface/core/compile/schema_renderers/prompt.py +29 -11
  49. dataface/core/compile/validator.py +8 -7
  50. dataface/core/compile/vega_config.py +8 -25
  51. dataface/core/dashboard.py +5 -3
  52. dataface/core/defaults/default_config.yml +11 -0
  53. dataface/core/defaults/palettes/categorical/category-6-tonal-blue.yml +2 -2
  54. dataface/core/defaults/palettes/categorical/category-6-tonal-brown.yml +1 -1
  55. dataface/core/defaults/palettes/categorical/category-6-tonal-green.yml +1 -1
  56. dataface/core/defaults/palettes/categorical/category-6-tonal-orange.yml +1 -1
  57. dataface/core/defaults/palettes/categorical/category-6-tonal-purple.yml +1 -1
  58. dataface/core/execute/adapters/adapter_registry.py +6 -13
  59. dataface/core/execute/adapters/csv_adapter.py +3 -3
  60. dataface/core/execute/adapters/dbt_adapter.py +8 -13
  61. dataface/core/execute/adapters/sql_adapter.py +11 -15
  62. dataface/core/execute/cache_keys.py +7 -2
  63. dataface/core/execute/duckdb_cache.py +2 -1
  64. dataface/core/execute/source_resolver.py +17 -4
  65. dataface/core/project_roots.py +32 -51
  66. dataface/core/render/chart/pipeline.py +0 -3
  67. dataface/core/render/chart/profile.py +11 -26
  68. dataface/core/render/chart/standard_renderer.py +3 -15
  69. dataface/core/render/chart/type_inference.py +3 -3
  70. dataface/core/render/faces.py +55 -1
  71. dataface/core/render/renderer.py +16 -1
  72. dataface/core/render/variable_controls.py +2 -0
  73. dataface/core/scoped_paths.py +14 -29
  74. dataface/core/serve/server.py +8 -1
  75. dataface/integrations/highlighting.py +112 -181
  76. dataface/integrations/markdown.py +4 -2
  77. {dataface-0.1.5.dev88.dist-info → dataface-0.1.5.dev151.dist-info}/METADATA +7 -8
  78. {dataface-0.1.5.dev88.dist-info → dataface-0.1.5.dev151.dist-info}/RECORD +81 -83
  79. dataface/agent_api/_init_templates/agents_dft_snippet.md +0 -26
  80. dataface/agent_api/_project_agents_md.py +0 -43
  81. dataface/core/compile/models/theme.py +0 -362
  82. {dataface-0.1.5.dev88.dist-info → dataface-0.1.5.dev151.dist-info}/WHEEL +0 -0
  83. {dataface-0.1.5.dev88.dist-info → dataface-0.1.5.dev151.dist-info}/entry_points.txt +0 -0
  84. {dataface-0.1.5.dev88.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.
@@ -694,7 +694,6 @@ layers:
694
694
  - type: bar # bar | line | area | circle | square | tick | rule | trail | rect | image | scatter
695
695
  y: actual
696
696
  label: Actual
697
- fill: "#0369a1" # static paint (use fill:, not color: {value:})
698
697
  - type: line
699
698
  y: target
700
699
  label: Target
@@ -703,7 +702,7 @@ layers:
703
702
  title: "Target"
704
703
  ```
705
704
 
706
- 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.
707
706
 
708
707
  ### Conditional formatting
709
708
 
@@ -845,7 +844,7 @@ variables:
845
844
  description: "Restrict every query to one region."
846
845
  options:
847
846
  static: [US, EU, APAC]
848
- default: US
847
+ # No default: starts on All regions
849
848
  ```
850
849
 
851
850
  Input types (14 total):
@@ -893,7 +892,7 @@ variables:
893
892
  product:
894
893
  input: select
895
894
  options:
896
- static: [All, Electronics, Clothing] # Hardcoded list
895
+ static: [Electronics, Clothing] # Hardcoded list; blank -- All -- is implicit
897
896
  # OR
898
897
  query: products_list # Query whose first column is the option list
899
898
  column: product_name # Optional: which column in that query
@@ -906,6 +905,8 @@ variables:
906
905
 
907
906
  Top-level option-source binding (alternative to `options:`): `column`, `query`, `dimension` (MetricFlow), `measure` (MetricFlow), `model` (dbt).
908
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
+
909
910
  Disabled forms:
910
911
 
911
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:
@@ -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