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
- 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>
- 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>
Add a Docker-based FastAPI proxy skeleton (charter §4) for parity across
dev / homelab VPS / fly.io, plus root .gitignore for Godot 4.7 and Python.
- api/Dockerfile: python:3.12-slim, non-root, PORT-aware CMD
- api/app/main.py: /health + five role endpoint stubs (§4)
- api/requirements.txt: fastapi/uvicorn/httpx/pydantic, pinned
- docker-compose.yml: local dev with live reload
- api/fly.toml: prod deploy stub with /health check
- api/.env.example: key lives in the proxy, never the client (§4)
- .gitignore: ignores .env, whitelists .env.example
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>