diff --git a/milestones/README.md b/milestones/README.md index 99ea9fd..cbec422 100644 --- a/milestones/README.md +++ b/milestones/README.md @@ -5,11 +5,14 @@ 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:** **[generation-numeric-tunables.md](generation-numeric-tunables.md)** — -narration / scene-generation temperature and context-byte tunables must -be YAML numbers (`true` used to become `1.0` / 1-byte context). +**Active:** **[validation-numeric-tunables.md](validation-numeric-tunables.md)** — +nested validation OCR / layout / av_sync / timing / story_end numerics +must be YAML numbers (`true` used to become `1`). **Shipped:** +- **[generation-numeric-tunables.md](generation-numeric-tunables.md)** — + narration / scene-generation temperature and context-byte tunables + must be YAML numbers (#117). - **[hint-segment-create-bool.md](hint-segment-create-bool.md)** — hint `docgen.segment.create` must be a YAML boolean (#116). - **[numeric-config-tunables.md](numeric-config-tunables.md)** — diff --git a/milestones/generation-numeric-tunables.md b/milestones/generation-numeric-tunables.md index 2bbfd06..5fe11bd 100644 --- a/milestones/generation-numeric-tunables.md +++ b/milestones/generation-numeric-tunables.md @@ -1,6 +1,6 @@ # Milestone: LLM generation numeric tunables must be YAML numbers -**Status:** Active +**Status:** Shipped **PR:** [#117](https://github.com/jmjava/documentation-generator/pull/117) **Depends on:** `milestones/hint-segment-create-bool.md` (PR #116), `milestones/numeric-config-tunables.md` (PR #115), diff --git a/milestones/validation-numeric-tunables.md b/milestones/validation-numeric-tunables.md new file mode 100644 index 0000000..fdb6e2e --- /dev/null +++ b/milestones/validation-numeric-tunables.md @@ -0,0 +1,50 @@ +# Milestone: nested validation numerics must be YAML numbers + +**Status:** Active +**PR:** [#118](https://github.com/jmjava/documentation-generator/pull/118) +**Depends on:** `milestones/generation-numeric-tunables.md` (PR #117), +`milestones/numeric-config-tunables.md` (PR #115) + +## Problem + +Top-level ``validation.max_drift_sec`` / ``max_freeze_ratio`` are +already typed. Nested check tunables still go through ``int()`` / +``float()``. A YAML **bool** is a subclass of ``int``, so: + +1. ``validation.av_sync.tolerance_sec: true`` became **1.0s** OCR + slack (vs default 3.0). +2. ``validation.ocr.min_confidence: true`` became **1**. +3. ``validation.story_end.max_early_sec: true`` became **1s**. + +A YAML list or string raised ``TypeError`` / ``ValueError`` inside +validate, not ``ConfigError`` at load. + +## Goal + +Fail closed at ``Config.from_yaml``. Present values of: + +- ``validation.ocr.sample_interval_sec`` / ``min_confidence`` +- ``validation.layout.min_spacing_px`` / ``edge_margin_px`` +- ``validation.av_sync.tolerance_sec`` / ``min_anchors_per_segment`` / + ``max_anchors_per_segment`` +- ``validation.timing_sync.max_tail_gap_sec`` / ``max_end_overrun_sec`` +- ``validation.story_end.max_early_sec`` / ``max_early_ratio`` + +must be YAML numbers (int or float, not bool). Missing keys keep +defaults. + +## Done when + +- [x] Present nested tunables must be YAML numbers. +- [x] Tests for bool ``tolerance_sec`` / ``max_early_sec``, list + ``min_confidence``, quoted ``max_tail_gap_sec``, and valid + numbers. +- [x] `ruff check src/ tests/` +- [x] `pytest tests/` (682 passed, 1 skipped) +- [x] `docgen benchmark` (no clock change; meets baseline) + +## Out of scope + +- ``validation.*.enabled`` / ``layout.check_overlap`` / + ``prefer_scene_spec_labels`` boolean gates. +- Coercing numeric strings into numbers. diff --git a/src/docgen/config.py b/src/docgen/config.py index 7f064ba..ee70d33 100644 --- a/src/docgen/config.py +++ b/src/docgen/config.py @@ -607,6 +607,42 @@ def __post_init__(self) -> None: label="validation.narration_lint.post_tts_deny_patterns", source=src, ) + for nkey in ("sample_interval_sec", "min_confidence"): + if ocr.get(nkey) is not None: + require_yaml_number( + ocr[nkey], label=f"validation.ocr.{nkey}", source=src + ) + layout = self._sub_block(validation, "layout", label="validation.layout") + for nkey in ("min_spacing_px", "edge_margin_px"): + if layout.get(nkey) is not None: + require_yaml_number( + layout[nkey], label=f"validation.layout.{nkey}", source=src + ) + for nkey in ( + "tolerance_sec", + "min_anchors_per_segment", + "max_anchors_per_segment", + ): + if avs.get(nkey) is not None: + require_yaml_number( + avs[nkey], label=f"validation.av_sync.{nkey}", source=src + ) + ts_sync = self._sub_block( + validation, "timing_sync", label="validation.timing_sync" + ) + for nkey in ("max_tail_gap_sec", "max_end_overrun_sec"): + if ts_sync.get(nkey) is not None: + require_yaml_number( + ts_sync[nkey], + label=f"validation.timing_sync.{nkey}", + source=src, + ) + story = self._sub_block(validation, "story_end", label="validation.story_end") + for nkey in ("max_early_sec", "max_early_ratio"): + if story.get(nkey) is not None: + require_yaml_number( + story[nkey], label=f"validation.story_end.{nkey}", source=src + ) def _source_label(self) -> str: return self.yaml_path.name if self.yaml_path else "docgen.yaml" diff --git a/tests/test_config.py b/tests/test_config.py index 5b2a8ff..6156f62 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -948,3 +948,65 @@ def test_from_yaml_generation_numeric_tunables_allowed(tmp_path: Path) -> None: assert c.raw["narration_from_source"]["max_context_bytes"] == 90000 assert c.raw["manim_scene_generation"]["temperature"] == 0.4 assert c.raw["manim_scene_generation"]["max_whisper_words_in_prompt"] == 12 + + +def test_from_yaml_bool_av_sync_tolerance_raises(tmp_path: Path) -> None: + p = tmp_path / "docgen.yaml" + p.write_text("validation:\n av_sync:\n tolerance_sec: true\n", encoding="utf-8") + with pytest.raises( + ConfigError, match="validation.av_sync.tolerance_sec must be a YAML number" + ): + Config.from_yaml(p) + + +def test_from_yaml_list_ocr_min_confidence_raises(tmp_path: Path) -> None: + p = tmp_path / "docgen.yaml" + p.write_text( + "validation:\n ocr:\n min_confidence:\n - 40\n", + encoding="utf-8", + ) + with pytest.raises( + ConfigError, match="validation.ocr.min_confidence must be a YAML number" + ): + Config.from_yaml(p) + + +def test_from_yaml_bool_story_end_max_early_sec_raises(tmp_path: Path) -> None: + p = tmp_path / "docgen.yaml" + p.write_text("validation:\n story_end:\n max_early_sec: true\n", encoding="utf-8") + with pytest.raises( + ConfigError, match="validation.story_end.max_early_sec must be a YAML number" + ): + Config.from_yaml(p) + + +def test_from_yaml_string_timing_sync_tail_gap_raises(tmp_path: Path) -> None: + p = tmp_path / "docgen.yaml" + p.write_text( + 'validation:\n timing_sync:\n max_tail_gap_sec: "3.0"\n', + encoding="utf-8", + ) + with pytest.raises( + ConfigError, match="validation.timing_sync.max_tail_gap_sec must be a YAML number" + ): + Config.from_yaml(p) + + +def test_from_yaml_validation_nested_numerics_allowed(tmp_path: Path) -> None: + p = tmp_path / "docgen.yaml" + p.write_text( + "validation:\n" + " ocr:\n sample_interval_sec: 3\n min_confidence: 50\n" + " layout:\n min_spacing_px: 12\n edge_margin_px: 18\n" + " av_sync:\n tolerance_sec: 2.5\n min_anchors_per_segment: 3\n" + " timing_sync:\n max_tail_gap_sec: 4.0\n max_end_overrun_sec: 1.5\n" + " story_end:\n max_early_sec: 30\n max_early_ratio: 0.4\n", + encoding="utf-8", + ) + c = Config.from_yaml(p) + assert c.ocr_config["sample_interval_sec"] == 3 + assert c.ocr_config["min_confidence"] == 50 + assert c.layout_config["min_spacing_px"] == 12 + assert c.av_sync_config["tolerance_sec"] == 2.5 + assert c.timing_sync_config["max_tail_gap_sec"] == 4.0 + assert c.story_end_config["max_early_sec"] == 30