scroll: tag the disembody + relinquish un-do events as Fable, not Frame — TDD

SIG_UNREADY ("disembodies POSS Significator") + SEA_RELINQUISHED ("relinquishes
POSS affinity with the X") are the character un-doing its Significator / Sea —
the un-do counterparts of the fable SIG_READY / SEA_DRAWN. They were stranded in
FABLE_VERBS' blind spot, so the scroll tagged them data-base="frame" with a bare
@handle instead of "@handle's character", and the Fable filter never caught them
(a disembody log showed under Frame, split from its embody pair).

Add both to FABLE_VERBS so they render data-base="fable" + the "@handle's
character" stub and ride the Fable filter with their pair. The billboard recent-
room EMBED still drops SIG_UNREADY noise (a verb-based exclude, untouched); the
redact-pair machinery is verb-keyed too, so is_fable has no functional effect
beyond display/filter.

TDD: drama ITs +2 — sig-unready + sea-relinquished render fable + character
stub; is_fable test moves both verbs to the fable group (only table/gate events
— deposits, chair assignment — stay frame). 59 drama + 241 billboard + 407 epic
ITs green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-06-08 23:54:51 -04:00
parent 04ab673cea
commit 80391b37c2
2 changed files with 40 additions and 8 deletions

View File

@@ -63,10 +63,16 @@ class GameEvent(models.Model):
] ]
# "Fable" events — the character-creation provenance (Significator / SkyDrive # "Fable" events — the character-creation provenance (Significator / SkyDrive
# / Sea of Cards) the scroll tags data-label="fable" instead of "frame", and # / Sea of Cards), INCL. their un-do counterparts (SIG_UNREADY "disembodies"
# prefixes with "@handle's character" (a stub for the character's name). The # + SEA_RELINQUISHED "relinquishes"): the scroll tags data-base="fable" (not
# SEA_RELINQUISHED redact-pair counterpart inherits its struck=redact label. # "frame") + prefixes "@handle's character" (a stub for the character's name).
FABLE_VERBS = frozenset({SIG_READY, SKY_SAVED, SEA_DRAWN}) # The un-do verbs are fable too so they ride the Fable filter with their
# ready/drawn pair — a disembody/relinquish is the character acting, not a
# table/gate frame event. A struck fable row still collapses to
# data-label="redact"; the per-base filter needs BOTH Fable AND Redact to show it.
FABLE_VERBS = frozenset({
SIG_READY, SIG_UNREADY, SKY_SAVED, SEA_DRAWN, SEA_RELINQUISHED,
})
room = models.ForeignKey( room = models.ForeignKey(
"epic.Room", on_delete=models.CASCADE, related_name="events", "epic.Room", on_delete=models.CASCADE, related_name="events",

View File

@@ -271,12 +271,15 @@ class GameEventModelTest(TestCase):
self.assertTrue(event.struck) self.assertTrue(event.struck)
def test_is_fable_for_character_creation_verbs(self): def test_is_fable_for_character_creation_verbs(self):
# SIG_READY / SKY_SAVED / SEA_DRAWN are fable; everything else is not. # The Significator / SkyDrive / Sea of Cards events — INCL. their un-do
for verb in (GameEvent.SIG_READY, GameEvent.SKY_SAVED, GameEvent.SEA_DRAWN): # counterparts (disembody / relinquish) — are fable; table/gate events
# (deposits, chair assignment) are not.
for verb in (GameEvent.SIG_READY, GameEvent.SIG_UNREADY,
GameEvent.SKY_SAVED, GameEvent.SEA_DRAWN,
GameEvent.SEA_RELINQUISHED):
self.assertTrue( self.assertTrue(
record(self.room, verb, actor=self.user).is_fable, verb) record(self.room, verb, actor=self.user).is_fable, verb)
for verb in (GameEvent.SLOT_FILLED, GameEvent.ROLE_SELECTED, for verb in (GameEvent.SLOT_FILLED, GameEvent.ROLE_SELECTED):
GameEvent.SEA_RELINQUISHED, GameEvent.SIG_UNREADY):
self.assertFalse( self.assertFalse(
record(self.room, verb, actor=self.user).is_fable, verb) record(self.room, verb, actor=self.user).is_fable, verb)
@@ -331,6 +334,29 @@ class GameEventModelTest(TestCase):
self.assertIn('data-label="redact"', html) self.assertIn('data-label="redact"', html)
self.assertIn('data-base="frame"', html) self.assertIn('data-base="frame"', html)
def test_scroll_tags_sig_unready_as_fable_with_character_actor(self):
# "disembodies POSS Significator" is the character un-readying its sig —
# a FABLE event (rides the Fable filter with SIG_READY), so it gets the
# "@handle's character" stub + data-base="fable", not a bare @handle frame.
self.user.username = "disco"
self.user.save(update_fields=["username"])
ev = record(self.room, GameEvent.SIG_UNREADY, actor=self.user)
html = self._render_scroll(ev)
self.assertIn('data-base="fable"', html)
self.assertIn('<span class="ev-char-stub">\'s character</span>', html)
self.assertIn("disembodies", html)
def test_scroll_tags_sea_relinquished_as_fable_with_character_actor(self):
# The let-go counterpart of SEA_DRAWN is fable too, so the relinquishment
# rides the Fable filter with its pair (not stranded under Frame).
self.user.username = "disco"
self.user.save(update_fields=["username"])
ev = record(self.room, GameEvent.SEA_RELINQUISHED, actor=self.user,
card_name="The Magician")
html = self._render_scroll(ev)
self.assertIn('data-base="fable"', html)
self.assertIn('<span class="ev-char-stub">\'s character</span>', html)
def test_sig_ready_prose_degrades_without_corner_rank(self): def test_sig_ready_prose_degrades_without_corner_rank(self):
# Old events recorded before this change have no corner_rank key # Old events recorded before this change have no corner_rank key
event = record(self.room, GameEvent.SIG_READY, actor=self.user, event = record(self.room, GameEvent.SIG_READY, actor=self.user,