diff --git a/milestones/README.md b/milestones/README.md index 3b084ef..a59b7c2 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:** **[image-generation-strings.md](image-generation-strings.md)** — -`image_generation.model` / `size` / `quality` must be YAML strings at -config load. +**Active:** **[ai-timestamp-strings.md](ai-timestamp-strings.md)** — +`ai.provider` / `timestamps.engine` / `tts.language` must be YAML strings +at config load. **Shipped:** +- **[image-generation-strings.md](image-generation-strings.md)** — + `image_generation.model` / `size` / `quality` must be YAML strings (#104). - **[image-empty-bytes.md](image-empty-bytes.md)** — `image-generate` must not write empty PNG bytes as success (#103). - **[hint-front-matter-yaml.md](hint-front-matter-yaml.md)** — diff --git a/milestones/ai-timestamp-strings.md b/milestones/ai-timestamp-strings.md new file mode 100644 index 0000000..304dace --- /dev/null +++ b/milestones/ai-timestamp-strings.md @@ -0,0 +1,40 @@ +# Milestone: leftover AI / timestamps / TTS language keys must be strings + +**Status:** Active +**PR:** [#105](https://github.com/jmjava/documentation-generator/pull/105) +**Depends on:** `milestones/image-generation-strings.md` (PR #104), +`milestones/tts-empty-audio.md` (PR #99), +`milestones/multi-host-ai-hardening.md` + +## Problem + +`tts.model` / `image_generation.model` are already strings at config load. +These sibling keys were not: + +1. **`ai.provider` / `base_url` / `api_key_env`** — a YAML list was + `str()`’d (`"['openai']"`) into `normalize_provider`. +2. **`timestamps.engine`** — a list became `"['local']"`, then + `resolve_engine` raised unknown-engine only when timestamps ran. +3. **`tts.language`** — a list was `str()`’d into xAI TTS/STT + `language`. + +## Goal + +Fail closed at `Config.from_yaml`. Missing keys still use defaults +(`ai.provider` openai, `timestamps.engine` local, Grok language `en`). + +## Done when + +- [x] Present `ai.provider` / `base_url` / `api_key_env` must be + non-empty YAML strings. +- [x] Present `timestamps.engine` must be a non-empty YAML string. +- [x] Present `tts.language` must be a non-empty YAML string. +- [x] Tests for list values of those keys. +- [x] `ruff check src/ tests/` +- [x] `pytest tests/` +- [x] `docgen benchmark` (no clock change) + +## Out of scope + +- Unknown `timestamps.engine` values still fail at `resolve_engine`. +- `manim.font` / `manim.quality` string typing is separate. diff --git a/milestones/image-generation-strings.md b/milestones/image-generation-strings.md index dd34669..c40fb7d 100644 --- a/milestones/image-generation-strings.md +++ b/milestones/image-generation-strings.md @@ -1,7 +1,7 @@ # Milestone: image_generation.model / size / quality must be strings -**Status:** Active -**PR:** pending +**Status:** Shipped +**PR:** #104 **Depends on:** `milestones/image-empty-bytes.md` (PR #103), `milestones/tts-empty-audio.md` (PR #99) diff --git a/src/docgen/config.py b/src/docgen/config.py index 68277ad..726cd40 100644 --- a/src/docgen/config.py +++ b/src/docgen/config.py @@ -289,6 +289,18 @@ def __post_init__(self) -> None: require_yaml_string(ig["size"], label="image_generation.size", source=src) if ig.get("quality") is not None: require_yaml_string(ig["quality"], label="image_generation.quality", source=src) + ai = self._block("ai") + if ai.get("provider") is not None: + require_yaml_string(ai["provider"], label="ai.provider", source=src) + if ai.get("base_url") is not None: + require_yaml_string(ai["base_url"], label="ai.base_url", source=src) + if ai.get("api_key_env") is not None: + require_yaml_string(ai["api_key_env"], label="ai.api_key_env", source=src) + ts = self._block("timestamps") + if ts.get("engine") is not None: + require_yaml_string(ts["engine"], label="timestamps.engine", source=src) + if tts.get("language") is not None: + require_yaml_string(tts["language"], label="tts.language", 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 36f0e20..30fd077 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -486,3 +486,31 @@ def test_from_yaml_list_image_generation_quality_raises(tmp_path: Path) -> None: p.write_text("image_generation:\n quality:\n - high\n", encoding="utf-8") with pytest.raises(ConfigError, match="image_generation.quality must be a YAML string"): Config.from_yaml(p) + + +def test_from_yaml_list_ai_provider_raises(tmp_path: Path) -> None: + p = tmp_path / "docgen.yaml" + p.write_text("ai:\n provider:\n - openai\n", encoding="utf-8") + with pytest.raises(ConfigError, match="ai.provider must be a YAML string"): + Config.from_yaml(p) + + +def test_from_yaml_list_ai_base_url_raises(tmp_path: Path) -> None: + p = tmp_path / "docgen.yaml" + p.write_text("ai:\n base_url:\n - https://api.openai.com/v1\n", encoding="utf-8") + with pytest.raises(ConfigError, match="ai.base_url must be a YAML string"): + Config.from_yaml(p) + + +def test_from_yaml_list_timestamps_engine_raises(tmp_path: Path) -> None: + p = tmp_path / "docgen.yaml" + p.write_text("timestamps:\n engine:\n - local\n", encoding="utf-8") + with pytest.raises(ConfigError, match="timestamps.engine must be a YAML string"): + Config.from_yaml(p) + + +def test_from_yaml_list_tts_language_raises(tmp_path: Path) -> None: + p = tmp_path / "docgen.yaml" + p.write_text("tts:\n language:\n - en\n", encoding="utf-8") + with pytest.raises(ConfigError, match="tts.language must be a YAML string"): + Config.from_yaml(p)