class_id is the rulebook's word; the world has callings — that was the whole §17
reconcile. The contract the Narrator reads should say what the world says. There
are no saves (M9) and no deployed clients, so this is as cheap as it will ever be
and strictly more expensive every milestone after.
race_id reaches the log because the AI must be able to describe the player:
api/app/prompts.py rendered 'a sellsword' and now renders 'a beastfolk cutpurse'.
It humanizes the id (the model must never read 'hedge_mage') and picks the right
article ('an elf', not 'a elf').
LogPlayer's hardcoded CLASSES const is deleted — the roster is Callings.IDS. A
parity test reads the JSON Schema from the client and asserts its enum equals the
code, so the two sides of the HTTP boundary cannot drift.
The deserter allowed three callings that no longer exist. He now allows all seven;
a thematic gate is a content decision for the Greywater authoring.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
55 lines
1.6 KiB
Python
55 lines
1.6 KiB
Python
import copy
|
|
|
|
from app.canon_log import validate_origin
|
|
|
|
VALID_ORIGIN = {
|
|
"schema_version": 1,
|
|
"id": "deserter",
|
|
"display_name": "The Deserter",
|
|
"description": "You walked away from a company that doesn't allow walking away.",
|
|
"start_location_id": "greywater_docks",
|
|
"situation": ["Arrived at Greywater by barge before dawn, hood up"],
|
|
"opening_facts": ["the player deserted the Iron Kettle mercenary company"],
|
|
"disposition_overrides": {"brannoc_thane": 40, "cadwyn_vell": 15},
|
|
"inventory_grants": [{"item_id": "worn_shortsword", "qty": 1}],
|
|
"start_quest_id": "find_the_ledger",
|
|
"build_constraints": {
|
|
"allowed_callings": ["sellsword", "cutpurse", "bonesetter"],
|
|
"luck_modifier": 0,
|
|
},
|
|
}
|
|
|
|
|
|
def test_valid_origin_passes():
|
|
assert validate_origin(VALID_ORIGIN) == []
|
|
|
|
|
|
def test_null_start_quest_is_allowed():
|
|
doc = copy.deepcopy(VALID_ORIGIN)
|
|
doc["start_quest_id"] = None
|
|
assert validate_origin(doc) == []
|
|
|
|
|
|
def test_unknown_class_is_rejected():
|
|
doc = copy.deepcopy(VALID_ORIGIN)
|
|
doc["build_constraints"]["allowed_callings"] = ["bard"]
|
|
assert validate_origin(doc) != []
|
|
|
|
|
|
def test_missing_required_field_is_rejected():
|
|
doc = copy.deepcopy(VALID_ORIGIN)
|
|
del doc["start_location_id"]
|
|
assert validate_origin(doc) != []
|
|
|
|
|
|
def test_extra_field_is_rejected():
|
|
doc = copy.deepcopy(VALID_ORIGIN)
|
|
doc["surprise"] = True
|
|
assert validate_origin(doc) != []
|
|
|
|
|
|
def test_disposition_override_key_must_match_id_pattern():
|
|
doc = copy.deepcopy(VALID_ORIGIN)
|
|
doc["disposition_overrides"] = {"Brannoc Thane": 40}
|
|
assert validate_origin(doc) != []
|