28 Commits

Author SHA1 Message Date
a9357fb787 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
2026-07-12 20:58:49 -05:00
5149e817f6 fix(contract): repair 3-arg LogPlayer callers + digest article bug
Three callers (main_window_shell.gd, narrate_harness.gd, npc_harness.gd)
still built LogPlayer with the OLD 3-arg shape (name, class_id,
luck_descriptor) after LogPlayer.new() was migrated to the 4-arg
(name, race_id, calling_id, luck_descriptor) shape. This was invisible
to both grep and the test suite: every ctor param carries a default, so
GDScript compiles a 3-arg call against a 4-arg signature without error —
it just silently mis-binds positionally. "sellsword" landed in the
race_id slot (rejected — not a race), the luck descriptor landed in the
calling_id slot (rejected — not a calling), and luck_descriptor itself
fell back to its default "". Every field in the emitted dict came out
empty, which 422s against the canon-log schema's minLength/enum
constraints. Grepping for class_id can't see this, because there is no
class_id token left anywhere — the bug is purely positional.

Also fixes prompts.py's _article(""), which returned "an" because
Python's "" in "aeiou" is True, producing a doubled-space/wrong-article
digest line when race_id is empty. _describe_player now builds its
descriptor from whichever of race/calling are present and omits the
clause entirely when both are absent.

Adds a schema-parity guard for origin.schema.json's inlined
allowed_callings enum, which duplicated the calling roster with no
runtime check tying it to Callings.IDS. Renames leftover "class"
vocabulary in test names/comments to "calling".

Regression coverage:
- client/tests/unit/test_entities.gd: ctor populates both race_id and
  calling_id; a calling passed into the race_id slot (the exact shape
  of the three broken call sites) yields an all-empty row.
- api/tests/test_prompts.py: empty-race and empty-race-and-calling
  digest rendering, asserting no doubled space and no dangling article.
- client/tests/unit/test_schema_parity.gd: origin.schema.json's
  allowed_callings enum matches Callings.IDS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
2026-07-12 20:34:05 -05:00
a668cf98eb refactor(contract)!: class_id -> calling_id, and the log gains race_id
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
2026-07-12 20:27:56 -05:00
56278a4831 feat(api): /VERSION single source read at startup + GET /version 2026-07-11 10:31:54 -05:00
86d1352b19 test(api): gated live smoke — Fenn answers /npc/speak in voice
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 13:07:24 -05:00
a2c654c047 content: author Fenn + the NPC role prompt body 2026-07-10 12:26:04 -05:00
0c3d64d068 feat(api): wire POST /npc/speak (422 unknown npc, 502 model error) 2026-07-10 12:22:07 -05:00
004cc21724 feat(api): npc.run service — thin drop-in on the model pipeline 2026-07-10 12:18:01 -05:00
76c04b45d2 feat(api): render_npc_digest — persona/knowledge/moves/utterance 2026-07-10 12:14:34 -05:00
6cd5003930 feat(api): route the npc role to its own model 2026-07-10 12:11:54 -05:00
69729f7673 feat(api): content.load_npc + content-root resolver 2026-07-10 12:08:50 -05:00
f7c51b79da fix(api): best-effort call logging + non-JSON 200 → ModelError (final-review)
- call_log._default_write: swallow any exception from the default file/
  stdout sink and report to stderr, so a log-write failure (disk full,
  bad permissions, misconfigured CALL_LOG_PATH) never turns a successful
  narration into a 500 (charter §13). Injected write= sinks (used by
  tests) are left to surface their own errors.
- ollama_client.chat: catch ValueError alongside httpx.HTTPError in the
  retry loop so a 200 response with a non-JSON body (JSONDecodeError is
  a ValueError subclass) counts as a failed attempt and falls through to
  ModelError after the one retry, instead of escaping chat() uncaught
  (charter §12 — one retry, then ModelError, nothing else escapes).
- main.py: update stale docstrings — /dm/narrate is now fully wired
  (routing + model call + logging); the other four roles remain stubs.

Regression tests added for both fixes (TDD: watched RED, then GREEN).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 08:45:39 -05:00
470ccb2935 test(api): gated live Ollama smoke (--run-live) + env docs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 08:36:56 -05:00
45d5f0360b test(api): keep test_endpoints hermetic after /dm/narrate wiring
/dm/narrate now makes a real Ollama call (Task 7). The role-endpoint
loop tests in test_endpoints.py had no mock, so every default pytest
run was silently hitting a live Ollama instance on localhost:11434 —
breaking the hermetic-suite constraint. Add an autouse fixture that
stubs narrate.ollama_client.chat and narrate.call_log.record so all
five endpoints stay hermetic; assertions (200 on valid, 422 on
invalid) are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 08:31:42 -05:00
5a35802075 feat(api): wire /dm/narrate — 200 prose | 502 model_error | 422 invalid log
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 08:31:38 -05:00
2027e7a9b9 feat(api): narrate service — render→route→call→log, returns raw prose
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 08:27:36 -05:00
2558e2a2a0 feat(api): author narrator system prompt (voice, [FACT] rule, no-numbers, no player agency) 2026-07-10 08:23:31 -05:00
15995c57ee feat(api): §10 JSON-lines call logging (full prompt + seed, injectable sink)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 08:20:00 -05:00
be133c4f3a feat(api): Ollama /api/chat client with one retry + typed ModelError
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 08:15:45 -05:00
4d8a19353e feat(api): prompt loader (header split) + curated canon-log digest
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 08:11:57 -05:00
42b3720721 feat(api): config + role→model routing (qwen3.5 narrator default, think off)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 08:08:56 -05:00
7f1d9fa39a chore(api): canon log follow-ups — hardening, unified 422, contract doc
- canon_log.py: stringify jsonschema error-path segments before sorting so a
  future schema mixing array indices and object keys can't TypeError (#1)
- main.py: RequestValidationError handler reshapes pydantic body failures into
  the same {"detail": {"canon_log_errors": [...]}} envelope as schema failures,
  so the client parses ONE 422 shape (#2a) + tests
- pytest.ini: filter the starlette/httpx TestClient deprecation → pristine output (#4)
- docs/canon-log.md: living contract for the client (Plan B) — schemas, the §7
  boundary, the unified error envelope, per-role injection (#3)

Full suite 26 passing, 0 warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 13:03:59 -05:00
e39d1a446e fix(api): constrain disposition_overrides keys; close docker paper cuts
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 12:54:01 -05:00
6d5510773a feat(api): validate canon log at every role endpoint (422 on invalid) 2026-07-09 12:42:03 -05:00
4aa65c9d4c feat(content): POC deserter origin + world fixtures with id-resolution check 2026-07-09 12:37:29 -05:00
e80a4071f6 fix(api): pattern-constrain humiliations id; broaden canon log boundary tests 2026-07-09 12:34:31 -05:00
843ab11a5c feat(api): canon log JSON Schema + validator, enforce §7 luck boundary
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 12:31:10 -05:00
710745e548 feat(api): origin seed JSON Schema + validator
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 12:26:39 -05:00