From a668cf98ebad282491d07c3517b5b2f2569bc90b Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Sun, 12 Jul 2026 20:27:56 -0500 Subject: [PATCH] refactor(contract)!: class_id -> calling_id, and the log gains race_id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8 --- api/app/prompts.py | 21 ++++++++++- api/tests/fixtures/canon_log_valid.json | 3 +- api/tests/test_live_smoke.py | 3 +- api/tests/test_npc.py | 2 +- api/tests/test_origin_schema.py | 4 +- api/tests/test_prompts.py | 26 ++++++++++++- client/scripts/canon_log/log_player.gd | 41 ++++++++++++++------- client/scripts/newgame/new_game.gd | 17 ++++++--- client/tests/unit/test_entities.gd | 24 +++++++----- client/tests/unit/test_new_game.gd | 26 ++++++------- client/tests/unit/test_round_trip.gd | 7 ++-- client/tests/unit/test_schema_parity.gd | 37 +++++++++++++++++++ client/tests/unit/test_schema_parity.gd.uid | 1 + content/origins/deserter.json | 3 +- docs/schemas/canon-log.schema.json | 8 +++- docs/schemas/origin.schema.json | 9 +++-- 16 files changed, 171 insertions(+), 61 deletions(-) create mode 100644 client/tests/unit/test_schema_parity.gd create mode 100644 client/tests/unit/test_schema_parity.gd.uid diff --git a/api/app/prompts.py b/api/app/prompts.py index 094f3ab..9225f70 100644 --- a/api/app/prompts.py +++ b/api/app/prompts.py @@ -12,6 +12,23 @@ from pathlib import Path _PROMPT_DIR = Path(__file__).resolve().parent.parent / "prompts" +def _humanize(id_: str) -> str: + """Ids are snake_case; the model should read prose. hedge_mage -> hedge-mage.""" + return id_.replace("_", "-") + + +def _article(word: str) -> str: + return "an" if word[:1].lower() in "aeiou" else "a" + + +def _describe_player(player: dict) -> str: + """'Aldric, a beastfolk cutpurse' — what the AI narrates the player as.""" + race = _humanize(player.get("race_id", "")) + calling = _humanize(player.get("calling_id", "")) + name = player.get("name", "") + return f"{name}, {_article(race)} {race} {calling}".strip() + + @lru_cache(maxsize=None) def system_prompt(role: str) -> str: text = (_PROMPT_DIR / f"{role}.md").read_text(encoding="utf-8") @@ -50,7 +67,7 @@ def render_digest(canon_log: dict) -> str: descriptor = player.get("luck_descriptor") if descriptor: lines.append(f"Fortune: {descriptor}") - lines.append(f"Player: {player.get('name', '')}, a {player.get('class_id', '')}") + lines.append(f"Player: {_describe_player(player)}") lines.append("") lines.append("Describe the scene as it is now.") @@ -104,7 +121,7 @@ def render_npc_digest( lines.append(f"Active quest: {quest.get('name', '')} — {quest.get('objective', '')}") player = canon_log.get("player", {}) - lines.append(f"The player is {player.get('name', '')}, a {player.get('class_id', '')}.") + lines.append(f"The player is {_describe_player(player)}.") lines.append("") lines.append(f'The player says to you: "{utterance}"') diff --git a/api/tests/fixtures/canon_log_valid.json b/api/tests/fixtures/canon_log_valid.json index b0587c7..a4e8996 100644 --- a/api/tests/fixtures/canon_log_valid.json +++ b/api/tests/fixtures/canon_log_valid.json @@ -2,7 +2,8 @@ "schema_version": 1, "player": { "name": "Aldric", - "class_id": "sellsword", + "race_id": "human", + "calling_id": "sellsword", "luck_descriptor": "Fortune spits on you" }, "location": { "id": "greywater_docks", "name": "the Greywater docks" }, diff --git a/api/tests/test_live_smoke.py b/api/tests/test_live_smoke.py index 887741b..2b4a681 100644 --- a/api/tests/test_live_smoke.py +++ b/api/tests/test_live_smoke.py @@ -30,7 +30,8 @@ def test_live_npc_speak_fenn(): log = { "player": { "name": "Aldric", - "class_id": "sellsword", + "race_id": "human", + "calling_id": "sellsword", "luck_descriptor": "the dice are kind", }, "location": {"id": "greywater_docks", "name": "Greywater Docks"}, diff --git a/api/tests/test_npc.py b/api/tests/test_npc.py index c4d4893..a547482 100644 --- a/api/tests/test_npc.py +++ b/api/tests/test_npc.py @@ -4,7 +4,7 @@ import app.npc as npc from app.ollama_client import ModelError LOG = { - "player": {"name": "Aldric", "class_id": "sellsword", "luck_descriptor": "kind"}, + "player": {"name": "Aldric", "race_id": "human", "calling_id": "sellsword", "luck_descriptor": "kind"}, "location": {"id": "greywater_docks", "name": "Greywater Docks"}, "party": [], "recent_events": [], "established_facts": [], "active_quests": [], "humiliations": [], diff --git a/api/tests/test_origin_schema.py b/api/tests/test_origin_schema.py index 036b58e..e6f9025 100644 --- a/api/tests/test_origin_schema.py +++ b/api/tests/test_origin_schema.py @@ -14,7 +14,7 @@ VALID_ORIGIN = { "inventory_grants": [{"item_id": "worn_shortsword", "qty": 1}], "start_quest_id": "find_the_ledger", "build_constraints": { - "allowed_classes": ["sellsword", "assassin", "priest"], + "allowed_callings": ["sellsword", "cutpurse", "bonesetter"], "luck_modifier": 0, }, } @@ -32,7 +32,7 @@ def test_null_start_quest_is_allowed(): def test_unknown_class_is_rejected(): doc = copy.deepcopy(VALID_ORIGIN) - doc["build_constraints"]["allowed_classes"] = ["bard"] + doc["build_constraints"]["allowed_callings"] = ["bard"] assert validate_origin(doc) != [] diff --git a/api/tests/test_prompts.py b/api/tests/test_prompts.py index d1fceb1..4831c56 100644 --- a/api/tests/test_prompts.py +++ b/api/tests/test_prompts.py @@ -22,7 +22,7 @@ def test_digest_has_the_narrative_context(): assert "Companions present: Brannoc Thane, Cadwyn Vell" in d assert "Active quest: The Missing Ledger — Find who took Fenn's ledger" in d assert "Fortune: Fortune spits on you" in d - assert "Player: Aldric, a sellsword" in d + assert "Player: Aldric, a human sellsword" in d assert d.rstrip().endswith("Describe the scene as it is now.") @@ -42,10 +42,32 @@ def test_narrator_body_is_authored(): assert "second person" in body # the core task is present +def _with_player(**player) -> dict: + log = json.loads(json.dumps(VALID)) # deep copy — VALID is module-level and shared + log["player"] = {"luck_descriptor": "the dice are kind", **player} + return log + + +def test_digest_names_the_race_and_the_calling(): + out = render_digest(_with_player(name="Aldric", race_id="beastfolk", calling_id="cutpurse")) + assert "Aldric, a beastfolk cutpurse" in out + + +def test_digest_says_an_elf_not_a_elf(): + out = render_digest(_with_player(name="Aldric", race_id="elf", calling_id="sellsword")) + assert "an elf sellsword" in out + + +def test_digest_does_not_leak_a_snake_case_id(): + out = render_digest(_with_player(name="Aldric", race_id="human", calling_id="hedge_mage")) + assert "hedge_mage" not in out, "the model must never read a raw snake_case id" + assert "hedge-mage" in out + + def test_render_npc_digest_includes_all_sections(): from app import prompts log = { - "player": {"name": "Aldric", "class_id": "sellsword", "luck_descriptor": "the dice are kind"}, + "player": {"name": "Aldric", "race_id": "human", "calling_id": "sellsword", "luck_descriptor": "the dice are kind"}, "location": {"id": "greywater_docks", "name": "Greywater Docks"}, "party": [], "recent_events": ["a gull screamed"], "established_facts": ["the eastern bridge is out"], diff --git a/client/scripts/canon_log/log_player.gd b/client/scripts/canon_log/log_player.gd index d0620df..d0a268c 100644 --- a/client/scripts/canon_log/log_player.gd +++ b/client/scripts/canon_log/log_player.gd @@ -1,33 +1,46 @@ class_name LogPlayer extends RefCounted -## A canon-log player row. Carries ONLY name, class_id, luck_descriptor (§7): -## no numeric Luck, no stats — those live in GameState and never reach the AI. - -const CLASSES := ["sellsword", "assassin", "priest"] +## A canon-log player row. Carries ONLY name, race_id, calling_id, luck_descriptor +## (§7): no numeric Luck, no attributes — those live in GameState and never reach the +## AI. race_id is here because the Narrator and NPCs must be able to describe the +## player ("a beastfolk cutpurse"); the mechanical sheet must not. var name: String -var class_id: String +var race_id: String +var calling_id: String var luck_descriptor: String -func _init(p_name := "", p_class_id := "", p_luck_descriptor := "") -> void: +func _init(p_name := "", p_race_id := "", p_calling_id := "", p_luck_descriptor := "") -> void: name = p_name luck_descriptor = p_luck_descriptor - if p_class_id != "": - set_class_id(p_class_id) + if p_race_id != "": + set_race_id(p_race_id) + if p_calling_id != "": + set_calling_id(p_calling_id) -func set_class_id(v: String) -> bool: - if v not in CLASSES: - push_error("invalid class_id: %s" % v) +func set_race_id(v: String) -> bool: + if not Races.exists(v): + push_error("invalid race_id: %s" % v) return false - class_id = v + race_id = v + return true + + +func set_calling_id(v: String) -> bool: + if not Callings.exists(v): + push_error("invalid calling_id: %s" % v) + return false + calling_id = v return true func to_dict() -> Dictionary: - return {"name": name, "class_id": class_id, "luck_descriptor": luck_descriptor} + return {"name": name, "race_id": race_id, "calling_id": calling_id, + "luck_descriptor": luck_descriptor} static func from_dict(d: Dictionary) -> LogPlayer: - return LogPlayer.new(d.get("name", ""), d.get("class_id", ""), d.get("luck_descriptor", "")) + return LogPlayer.new(d.get("name", ""), d.get("race_id", ""), + d.get("calling_id", ""), d.get("luck_descriptor", "")) diff --git a/client/scripts/newgame/new_game.gd b/client/scripts/newgame/new_game.gd index 3f52e46..a07e219 100644 --- a/client/scripts/newgame/new_game.gd +++ b/client/scripts/newgame/new_game.gd @@ -12,12 +12,16 @@ static func construct(origin: Dictionary, world: ContentDB, creation: Dictionary for ref in world.unresolved_refs(origin): errors.append("unresolved ref: %s" % ref) - # 2. build constraints — chosen class must be allowed by the origin. + # 2. build constraints — chosen calling must be allowed by the origin. var bc: Dictionary = origin.get("build_constraints", {}) - var allowed: Array = bc.get("allowed_classes", []) - var class_id: String = creation.get("class_id", "") - if class_id not in allowed: - errors.append("class not allowed by origin: %s" % class_id) + var allowed: Array = bc.get("allowed_callings", []) + var calling_id: String = creation.get("calling_id", "") + if calling_id not in allowed: + errors.append("calling not allowed by origin: %s" % calling_id) + + var race_id: String = creation.get("race_id", "") + if not Races.exists(race_id): + errors.append("unknown race: %s" % race_id) if creation.get("name", "").strip_edges() == "": errors.append("player name is required") @@ -36,7 +40,8 @@ static func construct(origin: Dictionary, world: ContentDB, creation: Dictionary # 5. assemble the canon log. var log := CanonLog.new() - log.player = LogPlayer.new(creation.get("name", ""), class_id, state.luck_descriptor()) + log.player = LogPlayer.new(creation.get("name", ""), race_id, calling_id, + state.luck_descriptor()) var loc: Dictionary = world.location(origin["start_location_id"]) log.set_location(loc.get("id", ""), loc.get("name", "")) diff --git a/client/tests/unit/test_entities.gd b/client/tests/unit/test_entities.gd index e18fa1a..c405c39 100644 --- a/client/tests/unit/test_entities.gd +++ b/client/tests/unit/test_entities.gd @@ -14,18 +14,22 @@ func test_ids_regex(): assert_false(Ids.is_valid("has-hyphen")) -func test_player_dict_has_only_three_keys(): - var p = LogPlayer.new("Aldric", "sellsword", "Fortune spits on you") - var d = p.to_dict() - assert_eq(d.keys().size(), 3) - assert_false("luck" in d) - assert_eq(d["class_id"], "sellsword") +func test_log_player_carries_race_and_calling_and_no_luck_number(): + var p := LogPlayer.new("Aldric", "beastfolk", "cutpurse", "Fortune spits on you") + var d := p.to_dict() + assert_eq(d["race_id"], "beastfolk") + assert_eq(d["calling_id"], "cutpurse") + assert_false("luck" in d, "numeric Luck never reaches the log (§7)") + assert_false("class_id" in d) + assert_eq(d.keys().size(), 4) -func test_player_rejects_unknown_class(): - var p = LogPlayer.new("Aldric", "sellsword", "x") - assert_false(p.set_class_id("bard")) - assert_eq(p.class_id, "sellsword") +func test_log_player_rejects_a_dead_calling(): + var p := LogPlayer.new() + assert_false(p.set_calling_id("priest"), "priest is not a calling") + assert_true(p.set_calling_id("bonesetter")) + assert_false(p.set_race_id("orc")) + assert_true(p.set_race_id("dwarf")) func test_party_member_clamps_disposition(): diff --git a/client/tests/unit/test_new_game.gd b/client/tests/unit/test_new_game.gd index 18049f4..dcef883 100644 --- a/client/tests/unit/test_new_game.gd +++ b/client/tests/unit/test_new_game.gd @@ -26,20 +26,20 @@ func _build(creation: Dictionary) -> Dictionary: func test_construct_succeeds(): - var res := _build({"name": "Aldric", "class_id": "sellsword"}) + var res := _build({"name": "Aldric", "race_id": "human", "calling_id": "sellsword"}) assert_true(res["ok"], str(res["errors"])) func test_numeric_luck_absent_from_log(): - var res := _build({"name": "Aldric", "class_id": "sellsword"}) + var res := _build({"name": "Aldric", "race_id": "human", "calling_id": "sellsword"}) var pd: Dictionary = res["log"].player.to_dict() assert_false("luck" in pd) - assert_eq(pd.keys().size(), 3) + assert_eq(pd.keys().size(), 4) assert_true(res["state"].luck >= Luck.MIN and res["state"].luck <= Luck.MAX) func test_inventory_and_dispo_land_in_state_not_log(): - var res := _build({"name": "Aldric", "class_id": "sellsword"}) + var res := _build({"name": "Aldric", "race_id": "human", "calling_id": "sellsword"}) assert_eq(res["state"].inventory.get("worn_shortsword", 0), 1) assert_eq(res["state"].purse_copper, 47, "the deserter is down to his last coin") assert_false("copper" in res["state"].inventory, "money is never an inventory row") @@ -47,20 +47,20 @@ func test_inventory_and_dispo_land_in_state_not_log(): func test_companion_dispositions_from_overrides(): - var res := _build({"name": "Aldric", "class_id": "sellsword"}) + var res := _build({"name": "Aldric", "race_id": "human", "calling_id": "sellsword"}) assert_eq(res["log"].party_member("brannoc_thane").disposition, 40) assert_eq(res["log"].party_member("cadwyn_vell").disposition, 15) func test_situation_and_facts_seeded(): - var res := _build({"name": "Aldric", "class_id": "sellsword"}) + var res := _build({"name": "Aldric", "race_id": "human", "calling_id": "sellsword"}) assert_eq(res["log"].recent_events.size(), 2) assert_true("the player deserted the Iron Kettle mercenary company" in res["log"].established_facts) assert_eq(res["log"].humiliations.size(), 0) func test_active_quest_resolved_from_world(): - var res := _build({"name": "Aldric", "class_id": "sellsword"}) + var res := _build({"name": "Aldric", "race_id": "human", "calling_id": "sellsword"}) assert_eq(res["log"].active_quests.size(), 1) assert_eq(res["log"].active_quests[0].id, "find_the_ledger") assert_eq(res["log"].active_quests[0].status, "active") @@ -69,12 +69,12 @@ func test_active_quest_resolved_from_world(): func test_null_start_quest_yields_no_quest(): var o := _deserter() o["start_quest_id"] = null - var res := NewGame.construct(o, world, {"name": "X", "class_id": "sellsword"}, _rng()) + var res := NewGame.construct(o, world, {"name": "X", "race_id": "human", "calling_id": "sellsword"}, _rng()) assert_eq(res["log"].active_quests.size(), 0) func test_class_not_allowed_is_rejected(): - var res := _build({"name": "X", "class_id": "berserker"}) + var res := _build({"name": "X", "race_id": "human", "calling_id": "berserker"}) assert_false(res["ok"]) assert_true(res["errors"].size() > 0) assert_null(res["log"]) @@ -83,18 +83,18 @@ func test_class_not_allowed_is_rejected(): func test_unresolved_origin_is_rejected(): var o := _deserter() o["start_location_id"] = "nowhere" - var res := NewGame.construct(o, world, {"name": "X", "class_id": "sellsword"}, _rng()) + var res := NewGame.construct(o, world, {"name": "X", "race_id": "human", "calling_id": "sellsword"}, _rng()) assert_false(res["ok"]) assert_true("unresolved ref: location:nowhere" in res["errors"]) func test_empty_name_is_rejected(): - var missing_name := _build({"class_id": "sellsword"}) + var missing_name := _build({"race_id": "human", "calling_id": "sellsword"}) assert_false(missing_name["ok"]) assert_true(missing_name["errors"].size() > 0) assert_null(missing_name["log"]) - var whitespace_name := _build({"name": " ", "class_id": "sellsword"}) + var whitespace_name := _build({"name": " ", "race_id": "human", "calling_id": "sellsword"}) assert_false(whitespace_name["ok"]) assert_true(whitespace_name["errors"].size() > 0) assert_null(whitespace_name["log"]) @@ -104,7 +104,7 @@ func test_non_companion_override_goes_to_state_not_log(): world.npcs["oda_fenn"] = {"id": "oda_fenn", "name": "Oda Fenn", "role": "npc"} var o := _deserter() o["disposition_overrides"] = {"brannoc_thane": 40, "oda_fenn": -25} - var res := NewGame.construct(o, world, {"name": "X", "class_id": "sellsword"}, _rng()) + var res := NewGame.construct(o, world, {"name": "X", "race_id": "human", "calling_id": "sellsword"}, _rng()) assert_true(res["ok"], str(res["errors"])) assert_eq(res["state"].npc_dispositions["oda_fenn"], -25) assert_null(res["log"].party_member("oda_fenn")) diff --git a/client/tests/unit/test_round_trip.gd b/client/tests/unit/test_round_trip.gd index 3fb1e3c..5108147 100644 --- a/client/tests/unit/test_round_trip.gd +++ b/client/tests/unit/test_round_trip.gd @@ -11,7 +11,7 @@ func _built_log(): var rng := RandomNumberGenerator.new() rng.seed = 3 var origin: Dictionary = ContentDB.load_json(ContentDB.origin_path("deserter")) - var res := NewGame.construct(origin, world, {"name": "Aldric", "class_id": "priest"}, rng) + var res := NewGame.construct(origin, world, {"name": "Aldric", "race_id": "human", "calling_id": "bonesetter"}, rng) assert_true(res["ok"], str(res["errors"])) return res["log"] @@ -28,9 +28,10 @@ func test_constructed_log_satisfies_contract_invariants(): var d: Dictionary = _built_log().to_dict() assert_eq(d["schema_version"], 1) assert_true(d["recent_events"].size() <= 5) - assert_eq(d["player"].keys().size(), 3) # exactly name/class_id/luck_descriptor + assert_eq(d["player"].keys().size(), 4) # exactly name/race_id/calling_id/luck_descriptor assert_false("luck" in d["player"]) # §7: no numeric luck - assert_true(d["player"]["class_id"] in ["sellsword", "assassin", "priest"]) + assert_true(d["player"]["calling_id"] in Callings.IDS) + assert_true(d["player"]["race_id"] in Races.IDS) for m in d["party"]: assert_true(m["disposition"] >= -100 and m["disposition"] <= 100) for q in d["active_quests"]: diff --git a/client/tests/unit/test_schema_parity.gd b/client/tests/unit/test_schema_parity.gd new file mode 100644 index 0000000..42f8512 --- /dev/null +++ b/client/tests/unit/test_schema_parity.gd @@ -0,0 +1,37 @@ +extends "res://addons/gut/test.gd" +## The canon log crosses the HTTP boundary, so its roster exists twice: in code +## (Callings.IDS / Races.IDS) and in the JSON Schema the API validates against. +## Nothing at runtime reconciles them. This does. + +const ContentDB = preload("res://scripts/content/content_db.gd") + + +func _player_schema() -> Dictionary: + var path := ProjectSettings.globalize_path("res://") \ + .path_join("../docs/schemas/canon-log.schema.json").simplify_path() + var doc: Variant = ContentDB.load_json(path) + assert_eq(typeof(doc), TYPE_DICTIONARY, "canon-log schema did not parse") + return doc["properties"]["player"] + + +func test_schema_calling_enum_matches_the_code(): + var enum_ids: Array = _player_schema()["properties"]["calling_id"]["enum"] + enum_ids.sort() + var code_ids: Array = Callings.IDS.duplicate() + code_ids.sort() + assert_eq(enum_ids, code_ids) + + +func test_schema_race_enum_matches_the_code(): + var enum_ids: Array = _player_schema()["properties"]["race_id"]["enum"] + enum_ids.sort() + var code_ids: Array = Races.IDS.duplicate() + code_ids.sort() + assert_eq(enum_ids, code_ids) + + +func test_schema_requires_race_and_calling(): + var required: Array = _player_schema()["required"] + assert_true("race_id" in required) + assert_true("calling_id" in required) + assert_false("class_id" in required, "class_id is the rulebook's word — it is gone") diff --git a/client/tests/unit/test_schema_parity.gd.uid b/client/tests/unit/test_schema_parity.gd.uid new file mode 100644 index 0000000..ec2cb5a --- /dev/null +++ b/client/tests/unit/test_schema_parity.gd.uid @@ -0,0 +1 @@ +uid://dond21vwh46rt diff --git a/content/origins/deserter.json b/content/origins/deserter.json index f68cc1a..03add65 100644 --- a/content/origins/deserter.json +++ b/content/origins/deserter.json @@ -19,7 +19,8 @@ ], "start_quest_id": "find_the_ledger", "build_constraints": { - "allowed_classes": ["sellsword", "assassin", "priest"], + "allowed_callings": ["sellsword", "reaver", "cutpurse", "trapper", + "hedge_mage", "bonesetter", "bloodsworn"], "luck_modifier": 0 } } diff --git a/docs/schemas/canon-log.schema.json b/docs/schemas/canon-log.schema.json index 9853159..8ecf4a5 100644 --- a/docs/schemas/canon-log.schema.json +++ b/docs/schemas/canon-log.schema.json @@ -13,10 +13,14 @@ "player": { "type": "object", "additionalProperties": false, - "required": ["name", "class_id", "luck_descriptor"], + "required": ["name", "race_id", "calling_id", "luck_descriptor"], "properties": { "name": { "type": "string", "minLength": 1 }, - "class_id": { "enum": ["sellsword", "assassin", "priest"] }, + "race_id": { "enum": ["human", "elf", "dwarf", "beastfolk"] }, + "calling_id": { + "enum": ["sellsword", "reaver", "cutpurse", "trapper", + "hedge_mage", "bonesetter", "bloodsworn"] + }, "luck_descriptor": { "type": "string", "minLength": 1 } } }, diff --git a/docs/schemas/origin.schema.json b/docs/schemas/origin.schema.json index 0485da9..7f6a445 100644 --- a/docs/schemas/origin.schema.json +++ b/docs/schemas/origin.schema.json @@ -39,12 +39,15 @@ "build_constraints": { "type": "object", "additionalProperties": false, - "required": ["allowed_classes", "luck_modifier"], + "required": ["allowed_callings", "luck_modifier"], "properties": { - "allowed_classes": { + "allowed_callings": { "type": "array", "minItems": 1, - "items": { "enum": ["sellsword", "assassin", "priest"] } + "items": { + "enum": ["sellsword", "reaver", "cutpurse", "trapper", + "hedge_mage", "bonesetter", "bloodsworn"] + } }, "luck_modifier": { "type": "integer" } }