spike(semif): consumer fit for Wyrd scene change and Cicada affect gate (no service change)
Harness consumer_fit.py runs a consumer's per-turn decisions over hand-labelled cases, with rotations, a content-free null control and tagged positive controls. Cicada: an input-only 'does this earn a visible reaction?' gate scored 30/31 with descriptive options and 19/31 with terse yes/no options. Scoped by Cicada's 2026-09-20 ruling (affect is emitted once, no mood-ring classifier). Wyrd: on 3 real seed graphs, the first place-change wording failed its positive controls (1/6 moves). A location-anchored rewording scored 21/21, and exit selection scored 18/21. Semif fits the choice, not writing the node.
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
"""Wyrd scene-change spike (2026-09-27): builds consumer_fit.py scenario files.
|
||||
|
||||
Today Wyrd decides a scene change inside the GM's big `gen` structured-output calls, two ways
|
||||
(wyrd backend/wyrd_backend/gm/gen_proposer.py, orchestrator.py, story_direction.py):
|
||||
R8 reflect's plot_direction.target_node, accepted only if it is an advanceable exit
|
||||
(plot_direction.intrude is advisory and never read)
|
||||
path B raw AddStoryNode + AdvanceToNode events, gated by nothing but prose: "Add + advance to
|
||||
a NEW node ONLY when the scene moves to a genuinely different place — never every turn
|
||||
(that would churn the backdrop)." archival-memory "Watch: GM story-node churn".
|
||||
The two semif decisions that would stand in for those, per turn:
|
||||
place has the scene moved to a genuinely different place? (the path-B gate, Wyrd's own rule)
|
||||
exit which advanceable exit, if any, does this turn move into? (R8 target selection;
|
||||
options = "stay" + the real exits, so semif can only ever name a legal target)
|
||||
Semif cannot WRITE the new node (title, summary, mood) or the steering directive; the GM keeps
|
||||
that. It only makes the typed choice.
|
||||
|
||||
Story graphs are real seeds from wyrd backend/.wyrd-data/canon (read immutable): The Fletcher
|
||||
(3de8f7da916d), The Toll Road (8ad5ebeae45f), The Last Vigil (8c3da0e2915a; that campaign had
|
||||
ZERO edges, so every real scene change went through path B; one edge is added here so R8 has
|
||||
something to choose). The turn texts are authored in the campaigns' own register (their opening
|
||||
narration is real); the transcripts themselves are gone from conv-api (session_not_found).
|
||||
Labels are ONE labeller's judgment (mine). "control" cases are unambiguous.
|
||||
python wyrd_cases.py # writes consumer-fit-2026-09-27/wyrd-scene-{fletcher,toll,vigil}.json
|
||||
"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
GRAPHS = {
|
||||
"fletcher": {
|
||||
"current": {"title": "The Idle Wait", "summary": "You have no contracts, no coin, and nowhere to be. "
|
||||
"The town is a pressure cooker.", "location": "Oakhaven crossroads"},
|
||||
"exits": [("meet_wren", "Approach Wren", "Meeting the Fletcher",
|
||||
"You find Wren working on arrows. She has a soldier's eyes."),
|
||||
("tavern", "Go to Tavern", "Tavern Noise",
|
||||
"The local tavern is loud with soldiers from both sides.")],
|
||||
},
|
||||
"toll": {
|
||||
"current": {"title": "The First Toll", "summary": "You arrive at the tollhouse in the pouring rain, "
|
||||
"presenting your sealed letter to Sera.", "location": "north road tollhouse"},
|
||||
"exits": [("departure", "Passage Granted/Denied", "Beyond the Toll",
|
||||
"You leave the tollhouse, either with or without Sera's blessing, continuing down the north road.")],
|
||||
},
|
||||
"vigil": {
|
||||
"current": {"title": "The Last Vigil", "summary": "The player stands in the inner sanctum, guarding the "
|
||||
"Book. Lynnae enters through a hidden alcove to discuss the encroaching demons.",
|
||||
"location": "sanctum interior"},
|
||||
"exits": [("outside", "Leave the sanctum", "Outside the Sanctum",
|
||||
"Lynnae and the player step out of the interior into the exterior surroundings.")],
|
||||
},
|
||||
}
|
||||
|
||||
# (id, graph, tag, player action, character reply, place label, exit label)
|
||||
CASES = [
|
||||
# ---------------- The Fletcher: current = The Idle Wait, at the crossroads.
|
||||
("f-idle-look", "fletcher", "control", "I lean on the fence post and watch the soldiers pass.",
|
||||
"Boots churn the mud as a column of Iron Legion pikemen tramps past the crossroads, eyes forward, "
|
||||
"drums muffled by the damp. Nobody spares you a glance. Across the square, a Sylvan Pact scout watches "
|
||||
"them go from the shadow of the well, and does not move until the last of them has passed.",
|
||||
"holds", "stay"),
|
||||
("f-plan-tavern", "fletcher", "case", "Maybe I'll try the tavern later, see if anyone's hiring.",
|
||||
"A gust carries the smell of woodsmoke and spilled ale from the tavern at the far end of the square. "
|
||||
"Someone inside is laughing too loudly. It would be warm in there, and full of men with coin and "
|
||||
"grudges. For now the crossroads holds you, and the rain has not yet decided whether to fall.",
|
||||
"holds", "stay"),
|
||||
("f-go-tavern", "fletcher", "control", "I head for the tavern and push the door open.",
|
||||
"The door gives with a groan and the noise hits you like heat: Legion sergeants at one long table, "
|
||||
"Pact rangers at another, and a narrow no-man's-land of spilled ale between them. The barkeep looks "
|
||||
"up, measures your empty belt, and goes back to wiping a cup that will never be clean.",
|
||||
"moved", "tavern"),
|
||||
("f-approach-wren", "fletcher", "control", "I walk over to the woman fletching arrows by the smithy.",
|
||||
"She doesn't look up as your shadow falls across her work. Her hands keep moving, binding grey goose "
|
||||
"feather to shaft with quick, sure turns. \"These are war arrows,\" she says, \"fletched for distance "
|
||||
"and accuracy, not for some merchant's target practice. You're either very brave or very stupid.\"",
|
||||
"holds", "meet_wren"),
|
||||
("f-memory", "fletcher", "case", "I think back to the last time I held a bow, at the siege of Varn.",
|
||||
"The memory comes unbidden: the walls of Varn black against a burning sky, the bowstring biting your "
|
||||
"fingers, the screaming from the breach. You flex your hand now, in the mud of Oakhaven, and find it "
|
||||
"still remembers the draw. Nobody at the crossroads notices a ghost remembering a war.",
|
||||
"holds", "stay"),
|
||||
("f-leave-town", "fletcher", "case", "Forget this town. I take the west road out of Oakhaven.",
|
||||
"The crossroads falls behind you, then the last hovels, then the smell of the town itself. By "
|
||||
"afternoon the west road narrows between dark pines, and the only sound is your own boots and, "
|
||||
"somewhere ahead, the slow creak of a mill wheel turning in water you cannot yet see.",
|
||||
"moved", "stay"),
|
||||
("f-nightfall", "fletcher", "case", "I stay put and wait for something to happen.",
|
||||
"Hours pass. The rain finally commits, then relents. Night settles over the crossroads, and one by "
|
||||
"one the shutters close. A lantern is lit at the well. Still nothing. Still waiting. Somewhere a dog "
|
||||
"barks twice and thinks better of it.",
|
||||
"holds", "stay"),
|
||||
("f-wren-comes", "fletcher", "case", "I keep watching the square.",
|
||||
"A woman with a quiver of fresh-fletched arrows crosses the square toward you, stepping neatly around "
|
||||
"the worst of the mud. She stops an arm's length away and studies you the way a quartermaster studies "
|
||||
"a horse. \"You've been standing there all morning,\" Wren says. \"Either you're waiting for work, or "
|
||||
"you're a spy. Which is it?\"",
|
||||
"holds", "meet_wren"),
|
||||
# ---------------- The Toll Road: current = The First Toll, in the tollhouse.
|
||||
("t-reads-letter", "toll", "control", "I hand Sera the sealed letter.",
|
||||
"Sera's gloved hand emerges from the shadow to take the letter, her fingers tracing the wax seal with "
|
||||
"practiced efficiency. She turns it over slowly, examining it by the light of the single lamp. Rain "
|
||||
"hammers the iron roof. \"Whose seal is this?\" she asks, without looking up.",
|
||||
"holds", "stay"),
|
||||
("t-questions", "toll", "case", "\"A friend in the capital. That's all you need to know.\"",
|
||||
"\"That's rarely all I need to know.\" Sera sets the letter on the counter between you and leaves her "
|
||||
"hand on it. \"The road north has been kind to liars lately. It won't stay that way.\" Water drips from "
|
||||
"the eaves into a bucket by the door, counting out the silence.",
|
||||
"holds", "stay"),
|
||||
("t-future", "toll", "case", "\"Stamp it and I'll be out of your hair before the rain stops.\"",
|
||||
"\"Before the rain stops,\" Sera repeats, as if tasting the words. \"It hasn't stopped in nine days.\" "
|
||||
"She doesn't reach for the stamp. She reaches for the ledger instead, and opens it to a page thick with "
|
||||
"names, and runs one gloved finger down the column.",
|
||||
"holds", "stay"),
|
||||
("t-granted", "toll", "control", "I wait while she decides.",
|
||||
"The stamp comes down with a crack like a gavel. Sera slides the letter back and jerks her chin at the "
|
||||
"door. You step out into the downpour, and the tollhouse lamp shrinks behind you as the north road "
|
||||
"unspools ahead, black and glistening, into the hills.",
|
||||
"moved", "departure"),
|
||||
("t-denied", "toll", "case", "\"Then I'll find another way north.\" I turn to go.",
|
||||
"\"There is no other way north,\" Sera says to your back, but she lets you go. The door bangs shut "
|
||||
"behind you. You walk on anyway, past the barrier, the rain soaking through your collar, the tollhouse "
|
||||
"dwindling to a single yellow window behind you on the north road.",
|
||||
"moved", "departure"),
|
||||
("t-rider", "toll", "case", "I stay by the stove to dry off.",
|
||||
"Hooves on the road outside, then a fist on the door. A rider in a sodden Legion cloak ducks in out of "
|
||||
"the rain, glances at you, glances at Sera, and drops a heavier purse on the counter than any toll "
|
||||
"requires. Sera's hand closes over it without a word.",
|
||||
"holds", "stay"),
|
||||
("t-city-memory", "toll", "case", "\"You've never been to the capital, have you?\"",
|
||||
"Something shifts behind Sera's eyes. \"I was born there,\" she says. \"Under the bell tower, in the "
|
||||
"year the river froze. I left the day they hanged my father in the square.\" She says it flatly, the "
|
||||
"way you'd read out a toll schedule, and turns back to the ledger.",
|
||||
"holds", "stay"),
|
||||
# ---------------- The Last Vigil: current = the inner sanctum.
|
||||
("v-talk", "vigil", "control", "\"How close are they?\"",
|
||||
"Lynnae rests her fingertips on the edge of the altar, not quite touching the Book. \"Close enough that "
|
||||
"the wards are singing,\" she says. \"Listen.\" You do, and beneath the wind there is a high, thin note, "
|
||||
"like a wet finger on the rim of a glass. The candles lean toward the door.",
|
||||
"holds", "stay"),
|
||||
("v-step-out", "vigil", "control", "\"Show me.\" I follow her out through the great doors.",
|
||||
"The doors groan open and the wind takes the candle flames behind you. Outside, the sanctum steps drop "
|
||||
"away into a valley of ash, and on the far ridge a line of red lights moves slowly, patiently, like a "
|
||||
"procession of lanterns that has forgotten what it is mourning.",
|
||||
"moved", "outside"),
|
||||
("v-plan-pub", "vigil", "case", "\"If the wards fall, we take the Book to the old pub in the village.\"",
|
||||
"\"The Drowned Crown?\" Lynnae almost smiles. \"No demon would look for holy relics behind a bar.\" She "
|
||||
"turns the idea over, then nods slowly. \"If it comes to that. But the wards have not fallen yet, and I "
|
||||
"would rather they never did.\"",
|
||||
"holds", "stay"),
|
||||
("v-to-pub", "vigil", "case", "I take the Book and we run for the village pub.",
|
||||
"You run through the ash with the Book clutched to your chest, Lynnae a step behind. The heavy door "
|
||||
"creaks on rusted hinges as you cross the threshold, the air inside the tavern thick with the scent of "
|
||||
"stale ale, damp rot, and cold ash. You set the Book down on the bar.",
|
||||
"moved", "stay"),
|
||||
("v-vision", "vigil", "case", "I touch the Book's cover.",
|
||||
"The world tilts. For one breath you are elsewhere: a burning library, a hundred years ago, monks "
|
||||
"carrying armfuls of scrolls through smoke. Then the vision snaps shut like a book, and you are back in "
|
||||
"the sanctum with Lynnae's hand hard on your wrist. \"Don't,\" she says. \"Not yet.\"",
|
||||
"holds", "stay"),
|
||||
("v-demon-enters", "vigil", "case", "I draw my blade and stand before the altar.",
|
||||
"The great doors burst inward. Ash rolls across the floor, and in the middle of it something tall "
|
||||
"unfolds itself, too many joints, a crown of red light. The candles gutter out all at once. Lynnae "
|
||||
"is already chanting, her voice steady, her hands trembling.",
|
||||
"holds", "stay"),
|
||||
]
|
||||
|
||||
|
||||
def build():
|
||||
cases = []
|
||||
for cid, graph, tag, action, reply, place, exit_ in CASES:
|
||||
g = GRAPHS[graph]
|
||||
exit_opts = [{"id": "stay", "description": f"None of these yet. The current scene holds: {g['current']['title']}."}]
|
||||
exit_opts += [{"id": eid, "description": f"{label}: {title}. {summary}"} for eid, label, title, summary in g["exits"]]
|
||||
cases.append({"id": cid, "tag": tag, "graph": graph,
|
||||
"state": {"current_scene": g["current"], "player_action": action, "narration": reply},
|
||||
"labels": {"place": place, "exit": exit_,
|
||||
"place2": {"holds": "same", "moved": "elsewhere"}[place], "exit2": exit_}})
|
||||
return cases
|
||||
|
||||
|
||||
# Round 2 wordings, written AFTER round 1 failed its positive controls on `place` (5 of 6 real
|
||||
# moves read as "holds", no better than the blind null). Both rounds are reported; round 1 is
|
||||
# kept in the same request so the comparison is paired.
|
||||
def round2(g):
|
||||
loc, title = g["current"]["location"], g["current"]["title"]
|
||||
place2 = {"id": "place2", "question": f"This is one turn of an interactive story. The turn began at the "
|
||||
f"{loc}. By the end of this turn's narration, where are the characters?",
|
||||
"options": [{"id": "same", "description": f"Still at the {loc}."},
|
||||
{"id": "elsewhere", "description": f"Somewhere else: they have left the {loc} for a different place."}]}
|
||||
exit2 = {"id": "exit2", "question": "This is one turn of an interactive story. The story can move on from the "
|
||||
"current scene into one of the listed next scenes, but only once the narration actually gets there. "
|
||||
"Where does this turn leave the story?",
|
||||
"options": [{"id": "stay", "description": f"Still in the current scene, {title}: the narration has "
|
||||
"not reached any next scene yet."}]
|
||||
+ [{"id": eid, "description": f"Moved on to {t}: {summ}"} for eid, _, t, summ in g["exits"]]}
|
||||
return [place2, exit2]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
here = Path(__file__).parent
|
||||
place = {"id": "place", "question": "This is one turn of an interactive story. Has the scene moved to a "
|
||||
"genuinely different place during this turn, or does the current scene still hold?",
|
||||
"options": [{"id": "moved", "description": "The scene moved to a genuinely different place."},
|
||||
{"id": "holds", "description": "The current scene still holds."}]}
|
||||
exit_q = ("This is one turn of an interactive story. The story can advance from the current scene into "
|
||||
"one of the listed next scenes. Which one does this turn move the story into?")
|
||||
# exit options differ per graph, so one scenario file per graph, each carrying every decision
|
||||
for graph, g in GRAPHS.items():
|
||||
cases = [c for c in build() if c["graph"] == graph]
|
||||
exit_opts = [{"id": "stay", "description": f"None of these yet. The current scene holds: {g['current']['title']}."}]
|
||||
exit_opts += [{"id": eid, "description": f"{label}: {title}. {summary}"} for eid, label, title, summary in g["exits"]]
|
||||
scen = {"name": f"wyrd-scene-{graph}",
|
||||
"decisions": [place, {"id": "exit", "question": exit_q, "options": exit_opts}] + round2(g),
|
||||
"cases": [{k: v for k, v in c.items() if k != "graph"} for c in cases]}
|
||||
(here / "consumer-fit-2026-09-27" / f"wyrd-scene-{graph}.json").write_text(json.dumps(scen, indent=1))
|
||||
Reference in New Issue
Block a user