From 29d11f3fa13512798094e6fad2962b3c2c130bfb Mon Sep 17 00:00:00 2001 From: Mikael Date: Tue, 16 Jun 2026 19:07:22 +0100 Subject: [PATCH 1/3] Add optional "Story Format" field on stories with validation Adds a "Story Format" field to TOStory: a high-level, objective classification of the narrative medium (not genre). The field is optional and may be omitted. When set, validate() now flags any value outside the permitted vocabulary: film, tv, play, prose, game, opera, nonfiction. Co-Authored-By: Claude Opus 4.8 --- tests/data/storyformat.st.txt | 43 +++++++++++++++++++++++++++++++++++ tests/test_totolo.py | 11 +++++++++ totolo/__init__.py | 2 +- totolo/impl/to_base.py | 24 +++++++++++++++++++ totolo/story.py | 1 + 5 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 tests/data/storyformat.st.txt diff --git a/tests/data/storyformat.st.txt b/tests/data/storyformat.st.txt new file mode 100644 index 0000000..5483e37 --- /dev/null +++ b/tests/data/storyformat.st.txt @@ -0,0 +1,43 @@ +movie: Good Format (2000) +========================= + +:: Title +Good Format + +:: Date +2000-01-01 + +:: Description +A story with a recognized story format. + +:: Story Format +film + + +movie: Bad Format (2001) +======================== + +:: Title +Bad Format + +:: Date +2001-01-01 + +:: Description +A story with an unrecognized story format. + +:: Story Format +graphic novel + + +movie: No Format (2002) +======================= + +:: Title +No Format + +:: Date +2002-01-01 + +:: Description +A story that omits the story format field entirely. diff --git a/tests/test_totolo.py b/tests/test_totolo.py index 47ac7df..7e55d94 100644 --- a/tests/test_totolo.py +++ b/tests/test_totolo.py @@ -406,6 +406,17 @@ 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_storyformat_warning(self): + ontology = totolo.files("tests/data/storyformat.st.txt") + warnings = list(ontology._impl.validate_storyformat()) + assert any("Bad Format" in x and "graphic novel" in x for x in warnings) + assert not any("Good Format" in x for x in warnings) + assert not any("No Format" in x for x in warnings) + # surfaces through the public validate() entry point as well + assert any( + "Unrecognized 'story format'" 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..13dd175 100644 --- a/totolo/__init__.py +++ b/totolo/__init__.py @@ -5,7 +5,7 @@ remote = TORemote() -__version__ = "2.1.3" +__version__ = "2.2.0" __ALL__ = [ empty, files, diff --git a/totolo/impl/to_base.py b/totolo/impl/to_base.py index 27eacc1..dc75059 100644 --- a/totolo/impl/to_base.py +++ b/totolo/impl/to_base.py @@ -10,6 +10,20 @@ from .to_containers import TODict +#: Permitted values for the optional "Story Format" field on a story. This is a +#: high-level, objective classification of the medium (not genre). The field may +#: be omitted entirely; if present it must be one of these values. +STORY_FORMATS = frozenset({ + "film", + "tv", + "play", + "prose", + "game", + "opera", + "nonfiction", +}) + + class TOBase(TOObject): story = a(TODict) theme = a(TODict) @@ -159,6 +173,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_storyformat() yield from self._impl.validate_cycles() def write(self, prefix=None, cleaned=False, verbose=False): @@ -299,6 +314,15 @@ def validate_storythemes(self): yield (f"{story.name}: Undefined '{weight} theme' with " f"name '{kwfield.keyword}'") + def validate_storyformat(self): + """Detect stories whose 'Story Format' is set to an unrecognized value.""" + for story in self.o.stories(): + value = str(story.get("Story Format")).strip() + if value and value not in STORY_FORMATS: + allowed = ", ".join(sorted(STORY_FORMATS)) + yield (f"{story.name}: Unrecognized 'story format' " + f"'{value}' (expected one of: {allowed})") + def validate_cycles(self): """Detect cycles (stops after first cycle encountered).""" parents = {} diff --git a/totolo/story.py b/totolo/story.py index 1e1d608..3006cbc 100644 --- a/totolo/story.py +++ b/totolo/story.py @@ -21,6 +21,7 @@ class TOStory(TOEntry): Collections = sa("list") Component_Stories = sa("list") Related_Stories = sa("list") + Story_Format = sa("text") Choice_Themes = sa("kwlist") Major_Themes = sa("kwlist") Minor_Themes = sa("kwlist") From cc230b80a69de073b01e6477922bed1e93f64326 Mon Sep 17 00:00:00 2001 From: Mikael Date: Tue, 16 Jun 2026 19:14:17 +0100 Subject: [PATCH 2/3] Restrict story format vocabulary to film, tv, play, prose, game --- totolo/impl/to_base.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/totolo/impl/to_base.py b/totolo/impl/to_base.py index dc75059..e1cb428 100644 --- a/totolo/impl/to_base.py +++ b/totolo/impl/to_base.py @@ -19,8 +19,6 @@ "play", "prose", "game", - "opera", - "nonfiction", }) From 30cf66a41ab032b7a8d8937d2f459005078c2516 Mon Sep 17 00:00:00 2001 From: Mikael Date: Tue, 16 Jun 2026 19:45:42 +0100 Subject: [PATCH 3/3] Use 'stage' (subsumes play/opera/musical) in story format vocabulary --- totolo/impl/to_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/totolo/impl/to_base.py b/totolo/impl/to_base.py index e1cb428..4fa8a70 100644 --- a/totolo/impl/to_base.py +++ b/totolo/impl/to_base.py @@ -16,7 +16,7 @@ STORY_FORMATS = frozenset({ "film", "tv", - "play", + "stage", "prose", "game", })