From c82fd870238d0e2bc7229bc7bdf20d2c69faaff4 Mon Sep 17 00:00:00 2001 From: Mikael Date: Tue, 16 Jun 2026 18:04:29 +0100 Subject: [PATCH] Validate dangling component-story references Add ThemeOntology validation that flags collection "Component Stories" entries pointing to stories not present in the ontology, mirroring the existing undefined-theme check. Wired into validate()/print_warnings. Bump version to 2.1.4. Co-Authored-By: Claude Opus 4.8 --- tests/data/dangling-component.st.txt | 28 ++++++++++++++++++++++++++++ tests/test_totolo.py | 13 +++++++++++++ totolo/__init__.py | 2 +- totolo/impl/to_base.py | 9 +++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 tests/data/dangling-component.st.txt diff --git a/tests/data/dangling-component.st.txt b/tests/data/dangling-component.st.txt new file mode 100644 index 0000000..036b520 --- /dev/null +++ b/tests/data/dangling-component.st.txt @@ -0,0 +1,28 @@ +movie: Real Movie (2000) +======================== + +:: Title +Real Movie + +:: Date +2000-01-01 + +:: Description +A real movie that exists as a story. + + +Collection: Test Collection +=========================== + +:: Title +Test Collection + +:: Date +2000 + +:: Description +A collection that lists one real story and one missing story. + +:: Component Stories +movie: Real Movie (2000) +movie: Missing Movie (1999) diff --git a/tests/test_totolo.py b/tests/test_totolo.py index 47ac7df..34b4414 100644 --- a/tests/test_totolo.py +++ b/tests/test_totolo.py @@ -406,6 +406,19 @@ def test_multiple_entries(self): warnings = list(ontology._impl.validate_entries()) assert any("Multiple TOStory with name" in x for x in warnings) + def test_component_warning(self): + ontology = totolo.files("tests/data/dangling-component.st.txt") + warnings = list(ontology._impl.validate_components()) + # The missing component story is flagged... + assert any( + "Undefined 'component story'" in x and "movie: Missing Movie (1999)" in x + for x in warnings + ) + # ...while the component story that exists is not. + assert not any("movie: Real Movie (2000)" in x for x in warnings) + # And it surfaces through the top-level validate(). + assert any("Undefined 'component story'" in x for x in ontology.validate()) + class TestOperations: def test_equality(self): diff --git a/totolo/__init__.py b/totolo/__init__.py index 06e37e8..8205d33 100644 --- a/totolo/__init__.py +++ b/totolo/__init__.py @@ -5,7 +5,7 @@ remote = TORemote() -__version__ = "2.1.3" +__version__ = "2.1.4" __ALL__ = [ empty, files, diff --git a/totolo/impl/to_base.py b/totolo/impl/to_base.py index 27eacc1..28c3417 100644 --- a/totolo/impl/to_base.py +++ b/totolo/impl/to_base.py @@ -159,6 +159,7 @@ def organize_collections(self): def validate(self): yield from self._impl.validate_entries() yield from self._impl.validate_storythemes() + yield from self._impl.validate_components() yield from self._impl.validate_cycles() def write(self, prefix=None, cleaned=False, verbose=False): @@ -299,6 +300,14 @@ def validate_storythemes(self): yield (f"{story.name}: Undefined '{weight} theme' with " f"name '{kwfield.keyword}'") + def validate_components(self): + """Detect component stories of collections that reference undefined stories.""" + for story in self.o.stories(): + for component_name in story.get("Component Stories"): + if component_name not in self.o.story: + yield (f"{story.name}: Undefined 'component story' with " + f"name '{component_name}'") + def validate_cycles(self): """Detect cycles (stops after first cycle encountered).""" parents = {}