Validate the generated deck at ``deck_dir/index.html``. Args: deck_dir: Directory containing ``index.html``. grammar: Optional skill-declared grammar (typically read from the skill's frontmatter under ``od.deck_grammar``). When ``None``, only skill-agnost
(
deck_dir: Path,
grammar: Optional[DeckGrammar] = None,
)
| 126 | |
| 127 | |
| 128 | def validate_deck( |
| 129 | deck_dir: Path, |
| 130 | grammar: Optional[DeckGrammar] = None, |
| 131 | ) -> ValidationResult: |
| 132 | """Validate the generated deck at ``deck_dir/index.html``. |
| 133 | |
| 134 | Args: |
| 135 | deck_dir: Directory containing ``index.html``. |
| 136 | grammar: Optional skill-declared grammar (typically read from the |
| 137 | skill's frontmatter under ``od.deck_grammar``). When ``None``, |
| 138 | only skill-agnostic invariants are checked (file present, |
| 139 | parses, ≥5 slides, self-contained). When provided, also |
| 140 | enforces required/allowed slide kinds. |
| 141 | |
| 142 | Returns a :class:`ValidationResult` with categorised issues. Never |
| 143 | raises for structural failures — those become entries in ``errors``. |
| 144 | """ |
| 145 | result = ValidationResult() |
| 146 | index = deck_dir / "index.html" |
| 147 | |
| 148 | if not index.is_file(): |
| 149 | result.errors.append(f"index.html not found at {index}") |
| 150 | return result |
| 151 | |
| 152 | size = index.stat().st_size |
| 153 | if size > MAX_FILE_BYTES: |
| 154 | result.warnings.append( |
| 155 | f"index.html is {size / 1024 / 1024:.1f} MB (> {MAX_FILE_BYTES // 1024 // 1024} MB) — " |
| 156 | f"likely too many inlined images." |
| 157 | ) |
| 158 | |
| 159 | text = index.read_text(encoding="utf-8", errors="replace") |
| 160 | kind_attr = grammar.get("kind_attr") if grammar else None |
| 161 | parser = _DeckParser(kind_attr=kind_attr) |
| 162 | try: |
| 163 | parser.feed(text) |
| 164 | parser.close() |
| 165 | except Exception as exc: |
| 166 | result.errors.append(f"index.html failed to parse: {exc}") |
| 167 | return result |
| 168 | |
| 169 | n = len(parser.slide_kinds) |
| 170 | |
| 171 | # ─── Skill-agnostic checks (always run) ────────────────────────────────── |
| 172 | if n < MIN_SLIDES_HARD: |
| 173 | result.errors.append( |
| 174 | f'deck has {n} slides; need at least {MIN_SLIDES_HARD} <section class="slide"> blocks.' |
| 175 | ) |
| 176 | |
| 177 | if parser.external_links: |
| 178 | result.errors.append( |
| 179 | "deck is not self-contained: external references found: " |
| 180 | + ", ".join(parser.external_links[:3]) |
| 181 | + ( |
| 182 | f", … (+{len(parser.external_links) - 3} more)" |
| 183 | if len(parser.external_links) > 3 |
| 184 | else "" |
| 185 | ) |