Skip to content

Commit 1203ba2

Browse files
authored
Merge pull request #94 from jmjava/cursor/wizard-exclude-lists-2ccd
Fail closed when wizard exclude_patterns is not a list
2 parents 2b0a369 + 15b9baf commit 1203ba2

7 files changed

Lines changed: 85 additions & 7 deletions

File tree

‎milestones/README.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ repositories that install `docgen` and maintain their own demo bundle. The
55
library no longer ships an in-repo dogfood; consumers are the integration test
66
of record.
77

8-
**Active:** **[timestamps-merge-stems.md](timestamps-merge-stems.md)** — CLI
9-
`timestamps` must merge stems into existing `timing.json`, not wipe extra keys
10-
or overwrite garbage JSON.
8+
**Active:** **[wizard-exclude-lists.md](wizard-exclude-lists.md)** —
9+
`wizard.exclude_patterns` / `scan_extensions` must be string lists, not scalars.
1110

1211
**Shipped:**
12+
- **[timestamps-merge-stems.md](timestamps-merge-stems.md)** — CLI `timestamps`
13+
merges stems into existing `timing.json` instead of wiping extra keys (#93).
1314
- **[asset-graph-timing.md](asset-graph-timing.md)** — wizard freshness must
1415
not treat corrupt `timing.json` as a missing timestamps entry (#92).
1516
- **[yaml-generate-mappings.md](yaml-generate-mappings.md)** — `yaml-generate`

‎milestones/timestamps-merge-stems.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Milestone: CLI timestamps must merge stems, not wipe `timing.json`
22

3-
**Status:** Active
4-
**PR:** pending
3+
**Status:** Shipped
4+
**PR:** #93
55
**Depends on:** `milestones/timestamps-fail-closed.md` (PR #78),
66
`milestones/timing-json-parse.md` (PR #89)
77

‎milestones/wizard-exclude-lists.md‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Milestone: wizard exclude/scan lists must not be scalars
2+
3+
**Status:** Active
4+
**PR:** pending
5+
**Depends on:** `milestones/yaml-generate-mappings.md` (PR #91),
6+
`milestones/config-mapping-keys.md` (PR #83)
7+
8+
## Problem
9+
10+
`wizard.exclude_patterns` is a list of glob strings. A YAML string was:
11+
12+
1. **Wiped** by `yaml-generate` `merge_defaults` (`ex = []`).
13+
2. **Iterated as characters** by wizard `/api/scan` (`fnmatch` over `"a"`, `"r"`, …).
14+
15+
`wizard.scan_extensions` as a string similarly overwrote the default extension
16+
tuple in `wizard_config` and scanned the wrong files.
17+
18+
## Goal
19+
20+
Fail closed at `Config.from_yaml` (`string_list_block`) and in `merge_defaults`
21+
(present non-list raises `ValueError`). Missing keys still default.
22+
23+
## Done when
24+
25+
- [x] `wizard.exclude_patterns` / `wizard.scan_extensions` if present must be
26+
YAML lists of strings.
27+
- [x] `merge_defaults` does not replace a string `exclude_patterns` with `[]`.
28+
- [x] Tests for string exclude_patterns and scan_extensions.
29+
- [x] `ruff check src/ tests/`
30+
- [x] `pytest tests/`
31+
- [x] `docgen benchmark` (no clock change)
32+
33+
## Out of scope
34+
35+
- Empty `exclude_patterns: []` remains allowed (archive dirs are still skipped
36+
via `is_under_archive_dir`).
37+
- Wizard `.docgen-state.json` corrupt still resets to empty.

‎src/docgen/config.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,21 @@ def __post_init__(self) -> None:
248248
f"{src}: visual_map.{sid_s} must be a YAML mapping, "
249249
f"not {type(spec).__name__}"
250250
)
251+
wiz = self._block("wizard")
252+
if wiz.get("exclude_patterns") is not None:
253+
string_list_block(
254+
wiz,
255+
"exclude_patterns",
256+
label="wizard.exclude_patterns",
257+
source=src,
258+
)
259+
if wiz.get("scan_extensions") is not None:
260+
string_list_block(
261+
wiz,
262+
"scan_extensions",
263+
label="wizard.scan_extensions",
264+
source=src,
265+
)
251266

252267
def _source_label(self) -> str:
253268
return self.yaml_path.name if self.yaml_path else "docgen.yaml"

‎src/docgen/yaml_generate.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,14 @@ def merge_defaults(
109109
raw["wizard"] = wiz
110110
else:
111111
wiz = _require_yaml_mapping(wiz, label="wizard")
112-
ex = wiz.setdefault("exclude_patterns", [])
113-
if not isinstance(ex, list):
112+
ex = wiz.get("exclude_patterns")
113+
if ex is None:
114114
ex = []
115115
wiz["exclude_patterns"] = ex
116+
elif not isinstance(ex, list):
117+
raise ValueError(
118+
f"wizard.exclude_patterns must be a YAML list, not {type(ex).__name__}"
119+
)
116120
if ARCHIVE_EXCLUDE not in ex:
117121
ex.append(ARCHIVE_EXCLUDE)
118122
changes.append(f"wizard.exclude_patterns: added {ARCHIVE_EXCLUDE!r}")

‎tests/test_config.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,3 +319,17 @@ def test_from_yaml_list_manim_scene_generation_raises(tmp_path: Path) -> None:
319319
p.write_text("manim_scene_generation: []\n", encoding="utf-8")
320320
with pytest.raises(ConfigError, match="manim_scene_generation must be a YAML mapping"):
321321
Config.from_yaml(p)
322+
323+
324+
def test_from_yaml_string_wizard_exclude_patterns_raises(tmp_path: Path) -> None:
325+
p = tmp_path / "docgen.yaml"
326+
p.write_text("wizard:\n exclude_patterns: archive\n", encoding="utf-8")
327+
with pytest.raises(ConfigError, match="wizard.exclude_patterns must be a YAML list"):
328+
Config.from_yaml(p)
329+
330+
331+
def test_from_yaml_string_wizard_scan_extensions_raises(tmp_path: Path) -> None:
332+
p = tmp_path / "docgen.yaml"
333+
p.write_text("wizard:\n scan_extensions: .md\n", encoding="utf-8")
334+
with pytest.raises(ConfigError, match="wizard.scan_extensions must be a YAML list"):
335+
Config.from_yaml(p)

‎tests/test_yaml_generate.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,13 @@ def test_merge_defaults_idempotent_archive(tmp_path: Path) -> None:
9494
assert not any("archive" in x and "added" in x for x in ch)
9595

9696

97+
def test_merge_defaults_rejects_string_wizard_exclude_patterns(tmp_path: Path) -> None:
98+
cfg = _minimal_cfg(tmp_path)
99+
raw = {"wizard": {"exclude_patterns": "**/archive/**"}, "discovery": {"auto_visual_map": False}}
100+
with pytest.raises(ValueError, match="wizard.exclude_patterns must be a YAML list"):
101+
merge_defaults(raw, cfg)
102+
103+
97104
def test_merge_defaults_syncs_manim_segments_from_visual_map(tmp_path: Path) -> None:
98105
raw = {
99106
"repo_root": ".",

0 commit comments

Comments
 (0)