Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions milestones/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)** —
Expand Down
40 changes: 40 additions & 0 deletions milestones/ai-timestamp-strings.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions milestones/image-generation-strings.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
12 changes: 12 additions & 0 deletions src/docgen/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
28 changes: 28 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)