From bbf87df7f8e2bb5bc19c628e6e1a42601e890e9b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 7 Sep 2026 21:15:02 +0000 Subject: [PATCH 1/2] Fail closed when env_file, repo_root, or dirs paths are a list YAML lists loaded, then Path joins TypeError'd in CLI env loading, wizard repo_root, or Config.__post_init__. Require those present keys to be non-empty YAML strings at config load. Co-authored-by: jmjava --- milestones/README.md | 8 ++++-- milestones/path-config-strings.md | 38 ++++++++++++++++++++++++++ milestones/visual-map-field-strings.md | 4 +-- src/docgen/config.py | 8 ++++++ tests/test_config.py | 21 ++++++++++++++ 5 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 milestones/path-config-strings.md diff --git a/milestones/README.md b/milestones/README.md index 95ca73c..bcf8c23 100644 --- a/milestones/README.md +++ b/milestones/README.md @@ -5,11 +5,13 @@ repositories that install `docgen` and maintain their own demo bundle. The library no longer ships an in-repo dogfood; consumers are the integration test of record. -**Active:** **[visual-map-field-strings.md](visual-map-field-strings.md)** — -`visual_map` type/scene/source and `segment_names` values must be YAML -strings at config load. +**Active:** **[path-config-strings.md](path-config-strings.md)** — +`env_file` / `repo_root` / `dirs.*` must be YAML strings at config load. **Shipped:** +- **[visual-map-field-strings.md](visual-map-field-strings.md)** — + `visual_map` type/scene/source and `segment_names` values must be YAML + strings (#108). - **[generation-model-strings.md](generation-model-strings.md)** — `narration_from_source` / `manim_scene_generation` model and prompt keys must be YAML strings (#107). diff --git a/milestones/path-config-strings.md b/milestones/path-config-strings.md new file mode 100644 index 0000000..72f4c5c --- /dev/null +++ b/milestones/path-config-strings.md @@ -0,0 +1,38 @@ +# Milestone: env_file / repo_root / dirs paths must be strings + +**Status:** Active +**PR:** pending +**Depends on:** `milestones/visual-map-field-strings.md` (PR #108) + +## Problem + +`manim.manim_path` is already a string at config load. These path keys +were not: + +1. **`env_file` / `repo_root`** — a YAML list loaded successfully, then + `Path / list` TypeError’d when CLI applied the env file or the wizard + resolved `repo_root`. +2. **`dirs.narration` / `audio` / `animations` / `recordings` / `hints`** + — a list TypeError’d during `Config.__post_init__` Path joins instead + of raising `ConfigError`. + +## Goal + +Fail closed at `Config.from_yaml` with `ConfigError`. Missing keys still +use defaults (`narration/`, git-discovered repo root, no `env_file`). + +## Done when + +- [x] Present `env_file` / `repo_root` must be non-empty YAML strings. +- [x] Present `dirs.narration` / `audio` / `animations` / `recordings` / + `hints` must be non-empty YAML strings. +- [x] Tests for list values of those keys. +- [ ] `ruff check src/ tests/` +- [ ] `pytest tests/` +- [ ] `docgen benchmark` (no clock change) + +## Out of scope + +- `wizard.default_guidance` type gating is separate. +- Per-segment generation `system_prompt` / `class_name` are separate. +- `pages` output directory keys are separate. diff --git a/milestones/visual-map-field-strings.md b/milestones/visual-map-field-strings.md index 7715e52..4654810 100644 --- a/milestones/visual-map-field-strings.md +++ b/milestones/visual-map-field-strings.md @@ -1,7 +1,7 @@ # Milestone: visual_map type/scene/source and segment_names values must be strings -**Status:** Active -**PR:** [#108](https://github.com/jmjava/documentation-generator/pull/108) +**Status:** Shipped +**PR:** #108 **Depends on:** `milestones/generation-model-strings.md` (PR #107), `milestones/visual-map-row-types.md` (PR #86), `milestones/segment-id-strings.md` (PR #87) diff --git a/src/docgen/config.py b/src/docgen/config.py index 334821a..4a52508 100644 --- a/src/docgen/config.py +++ b/src/docgen/config.py @@ -164,7 +164,11 @@ class Config: hints_dir: Path = field(init=False) def __post_init__(self) -> None: + src = self._source_label() dirs = self._block("dirs") + for dkey in ("narration", "audio", "animations", "recordings", "hints"): + if dirs.get(dkey) is not None: + require_yaml_string(dirs[dkey], label=f"dirs.{dkey}", source=src) self.narration_dir = self.base_dir / dirs.get("narration", "narration") self.audio_dir = self.base_dir / dirs.get("audio", "audio") self.animations_dir = self.base_dir / dirs.get("animations", "animations") @@ -347,6 +351,10 @@ def __post_init__(self) -> None: f"{src}: manim_scene_generation.scene_spec_system_prompt must be a " f"YAML string, not {type(msg['scene_spec_system_prompt']).__name__}" ) + if self.raw.get("env_file") is not None: + require_yaml_string(self.raw["env_file"], label="env_file", source=src) + if self.raw.get("repo_root") is not None: + require_yaml_string(self.raw["repo_root"], label="repo_root", source=src) ocr = self._sub_block(validation, "ocr", label="validation.ocr") if ocr.get("error_patterns") is not None: string_list_block( diff --git a/tests/test_config.py b/tests/test_config.py index eaa301d..2d0f738 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -631,3 +631,24 @@ def test_from_yaml_list_segment_names_value_raises(tmp_path: Path) -> None: p.write_text('segment_names:\n "01":\n - 01-intro\n', encoding="utf-8") with pytest.raises(ConfigError, match="segment_names.01 must be a YAML string"): Config.from_yaml(p) + + +def test_from_yaml_list_env_file_raises(tmp_path: Path) -> None: + p = tmp_path / "docgen.yaml" + p.write_text("env_file:\n - .env\n", encoding="utf-8") + with pytest.raises(ConfigError, match="env_file must be a YAML string"): + Config.from_yaml(p) + + +def test_from_yaml_list_repo_root_raises(tmp_path: Path) -> None: + p = tmp_path / "docgen.yaml" + p.write_text("repo_root:\n - ..\n", encoding="utf-8") + with pytest.raises(ConfigError, match="repo_root must be a YAML string"): + Config.from_yaml(p) + + +def test_from_yaml_list_dirs_narration_raises(tmp_path: Path) -> None: + p = tmp_path / "docgen.yaml" + p.write_text("dirs:\n narration:\n - narration\n", encoding="utf-8") + with pytest.raises(ConfigError, match="dirs.narration must be a YAML string"): + Config.from_yaml(p) From b4f39c64463df30b57ecb28725450cf3bc39ada2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 7 Sep 2026 21:15:34 +0000 Subject: [PATCH 2/2] Mark path-config-strings milestone gates as run ruff, pytest (644 passed, 1 skipped), and docgen benchmark are green. Co-authored-by: jmjava --- milestones/path-config-strings.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/milestones/path-config-strings.md b/milestones/path-config-strings.md index 72f4c5c..ed92f3e 100644 --- a/milestones/path-config-strings.md +++ b/milestones/path-config-strings.md @@ -1,7 +1,7 @@ # Milestone: env_file / repo_root / dirs paths must be strings **Status:** Active -**PR:** pending +**PR:** [#109](https://github.com/jmjava/documentation-generator/pull/109) **Depends on:** `milestones/visual-map-field-strings.md` (PR #108) ## Problem @@ -27,9 +27,9 @@ use defaults (`narration/`, git-discovered repo root, no `env_file`). - [x] Present `dirs.narration` / `audio` / `animations` / `recordings` / `hints` must be non-empty YAML strings. - [x] Tests for list values of those keys. -- [ ] `ruff check src/ tests/` -- [ ] `pytest tests/` -- [ ] `docgen benchmark` (no clock change) +- [x] `ruff check src/ tests/` +- [x] `pytest tests/` +- [x] `docgen benchmark` (no clock change) ## Out of scope