fix(creation-model): migrate stray class_id/allowed_classes copies

The canon-log schema and NewGame code already migrated to race_id/
calling_id + allowed_callings, but two copies of the roster outside
that schema were missed, and no parity test guards them:

- docs/canon-log.md (the cross-boundary contract doc) still documented
  the OLD player shape and allowed_classes. Anyone building a player
  block from the doc would get a 422.
- .claude/skills/world-building's schema reference still emitted
  allowed_classes with two dead calling ids (assassin, priest). Author
  a new origin with that skill and it fails origin.schema.json
  (additionalProperties: false, allowed_callings required) AND, if that
  ever loosened, silently allows zero callings at runtime.

Also:
- add schema tests rejecting a dead class_id field and an unknown
  calling_id (priest)
- guard NewGame._validate's container types (spend/skills) so a
  malformed JSON round-trip (null spend, string skills) produces a
  front-loaded error list instead of a GDScript runtime crash
- extend the no-mechanics-in-content test to db.races, not just
  db.callings
- rewrite the roadmap's M4 bullet: the two contract migrations landed;
  only the creation screen and title->creation->shell flow remain

client: 250/250. api: 74 passed, 2 skipped. content_build --check: clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
This commit is contained in:
2026-07-12 20:58:49 -05:00
parent 62b8113ffa
commit a9357fb787
7 changed files with 63 additions and 9 deletions

View File

@@ -274,6 +274,21 @@ func test_empty_name_is_rejected():
assert_null(whitespace_name["log"])
func test_a_null_spend_is_rejected_not_crashed():
# A JSON round-trip can hand back {"spend": null}. Without a container-type
# guard, assigning null into a statically-typed Dictionary parameter throws
# a GDScript runtime error INSIDE construct() instead of a front-loaded error.
var res := _build(_creation({"spend": null}))
assert_false(res["ok"])
assert_true("spend" in str(res["errors"]).to_lower())
func test_non_array_skills_is_rejected_not_crashed():
var res := _build(_creation({"skills": "athletics"}))
assert_false(res["ok"])
assert_true("skills" in str(res["errors"]).to_lower())
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()