"""Keep the 3D model review scorecard honest. Every catalogue asset (characters, gear, props) must have a row in `docs/assets/3d-model-review.md`, and the character rows must carry a verdict. This does not judge the art; it only fails when an asset is added or renamed without being reviewed, which is how a scorecard silently rots. """ import re import unittest from pathlib import Path import blender_assets as model ROOT = Path(model.__file__).resolve().parent.parent REVIEW = ROOT / "docs" / "assets" / "3d-model-review.md" def catalogue_names() -> set[str]: return set(model.CHARACTERS) | set(model.GEAR) | set(model.PROPS) def reviewed_rows(text: str) -> dict[str, list[str]]: """Map asset name -> table cells for rows written as ``| `name` | ... |``.""" rows = {} for line in text.splitlines(): match = re.match(r"^\| `([a-z0-9_]+)` \|(.*)\|\s*$", line) if match: rows[match.group(1)] = [cell.strip() for cell in match.group(2).split("|")] return rows class ModelReviewTests(unittest.TestCase): def test_every_catalogue_asset_has_a_scorecard_row(self): text = REVIEW.read_text(encoding="utf-8") rows = reviewed_rows(text) missing = sorted(catalogue_names() - set(rows)) self.assertEqual(missing, [], f"no scorecard row for: {', '.join(missing)}") def test_character_rows_carry_a_verdict(self): rows = reviewed_rows(REVIEW.read_text(encoding="utf-8")) for character in model.CHARACTERS: self.assertIn(character, rows, character) verdict = rows[character][-1] self.assertTrue(verdict, f"{character} has no verdict") self.assertNotEqual(verdict, "—", f"{character} verdict not filled in") def test_gear_and_prop_rows_have_a_verdict_column(self): rows = reviewed_rows(REVIEW.read_text(encoding="utf-8")) for prop in model.PROPS: self.assertIn(prop, rows, prop) self.assertTrue(rows[prop][-1], f"{prop} has no verdict column") if __name__ == "__main__": unittest.main()