Signup accepts only player/spectator — staff roles come from the
VTT_ADMIN_EMAILS allowlist (rotatable without restart). Gateway aborts
startup without AUTH_SECRET/VTT_ENGINE_SECRET, mirroring engine semantics.
GET /lobbies/{id} member-or-staff only with no existence oracle; invite
codes stay visible to members. 20 tests red-first; pytest 672 passed.
Co-Authored-By: Claude <[email protected]>
All narrative-facing /engine/* proxies resolve callers via _require_auth and
forward verified identity (_caller_actor); optional-token service-principal
fallback removed. 28-route anonymous-401 contract test matrix. Client now
attaches ?token=/Bearer on session/attack/check/map/metrics/presence with an
honest NOT_AUTHENTICATED rejection when signed out. Full benchmark run green:
MCR 100%, HCI 1.0, AFPR 0.0%, recall 100%, trust probes 10/10.
Co-Authored-By: Claude <[email protected]>
EngineSpawnEntity mirrors vtt-core EntityState field-for-field with
extra=forbid; malformed/smuggled spawn bodies now 422 instead of
reaching the engine. Deploy-path class attack derivation collapsed
into one disclosed helper (_derive_class_attack) pending a Rust-side
class-aware EntitySpec follow-up. 11 new tests, red-first.
Co-Authored-By: Claude <[email protected]>
Closes the close-out audit's top known-limit: quest generation
replaces the shared active_campaign_quest global every player's
journal reads, yet took no token — any unauthenticated caller could
rewrite the table's quest. Generate now requires GM/admin (403
QUEST_GENERATION_GM_ONLY, reject before mutation); active-quest read
and Concordia negotiation require authentication with no role gate.
7 red-first tests; suite at 558.
Co-Authored-By: Claude <[email protected]>
POST /api/v1/campaign/autosave: fail-closed order — role check before
any touch (403 AUTOSAVE_GM_ONLY), identity-forwarded engine fetch
(502 honest when down), verbatim snapshot persisted through the
existing save path owned by the caller. Repeated autosaves upsert one
rolling per-session slot instead of growing rows. 4 red-first tests;
suite at 551.
Co-Authored-By: Claude <[email protected]>
- Gate reliability: autouse conftest fixture clears the shared auth
rate-limit windows around every test; full suite now 3x-consecutive
green (was nondeterministically red via cross-module bucket pressure).
- Test quality over claim-laundering: computedHP gains the real SRD
hit-die table (Wizard/Sorcerer d6, Barbarian d12, per-die average
growth) — the old flat d8 fallback had been pinned by a test falsely
citing "hit-die max"; tests now cite SRD values explicitly.
- Scripted-mode social phase force-pins llm_gateway=None so an
LLM-gatewayed persona can't leak upstream calls the report claims
never happened; no-network test extended with a recording gateway.
- Dynasty test tautology replaced with real lineage assertions.
- Ambience-honesty test derives expected ids from AudioManager recipes
(source of truth) with a drift guard, not the copied list.
- Tracker honesty note appended to iteration 79: default quest output
changed materially; only quest_id and summary preserved.
Gates: pytest 547x3 green, vitest 69 green, build 13.3s.
Co-Authored-By: Claude <[email protected]>
Turns iteration 31's demo-data finding into a real backend: handouts
table via the idempotent DDL pattern, full CRUD under /api/v1/handouts
with visibility enforced server-side from the HMAC-verified role —
players/spectators see only all+party rows in SQL and process
(defense-in-depth), gm_only content 404s indistinguishably from
nonexistent for unauthorized callers (no existence oracle), updates/
deletes creator-or-GM. Typed client store mirrors lobby conventions.
8 red-first tests; suite at 547. Shared auth rate-limit bucket noted
as a future test-isolation follow-up.
Co-Authored-By: Claude <[email protected]>
Pillar 8 depth: extend_lineage compounds trait inheritance across
generations (0.65 base + 0.08/ancestor, capped 0.95) with atavism for
ancestral traits; form_alliance marries houses with dominance-weighted
trait pools (Iron Will 0.70 → Cursed Bloodline 0.15); deterministic
house_prestige from traits, pacts, feuds, and unbroken-bloodline depth;
lore injection emits generation-appropriate canon (young bloodline as
PROPOSED_FACT vs N-generation UNBROKEN as VALIDATED_CANON). Legacy API
and seed output pinned unchanged; probabilistic gates asserted exactly
via ScriptedRandom. 12 red-first tests; suite at 539.
Co-Authored-By: Claude <[email protected]>
The quest engine built one hardcoded intrigue DAG regardless of theme
args — rng instantiated but never used. Now: three curated theme tables
(court/crypt/wilderness with a new TRAVEL_HAZARD node type) drive node
types, titles, prompts, skills, and factions; party level scales DCs
and rewards; length plans 8/10/12-node DAGs; seed drives all selection
deterministically. Unknown themes raise instead of silently
substituting; thin tables truncate with an explicit coverage_note
rather than padding with invented filler. Legacy call signature and
default output preserved. 20 red-first tests; suite at 527.
Co-Authored-By: Claude <[email protected]>
Pillar 5 depth: between combat rounds, players attempt one dialogue
against a designated social NPC — LLM decisions via the same strict-
JSON gateway path with stance-branched scripted fallback, replies from
injectable ConcordiaNPC personas (registry=None skips the phase
entirely, never faked), approaches mapped onto disposition kinds so
repeated hostility visibly shifts stances across rounds. Norms are
absolute in both directions: violating utterances never reach the NPC,
violating replies degrade to template — both surfaced as norm_rejected.
Deterministic sim-clock timestamps; 11 red-first tests; suite at 526.
Co-Authored-By: Claude <[email protected]>
- F1 MED: Help promise burned by LOS/cover-refused attacks —
consumption moved after all fallible gates; refused attacks leave it
standing (red-first socket test).
- F2 MED: explicit under-level cast requests now 422 INVALID_SLOT_LEVEL
instead of silent .max() upgrade (gateway default aligned to 0 =
unspecified); absurd damage formulas (>2x caps) hard-rejected without
spending slots, moderate overshoot still gently clamped — validation
hoisted before expenditure.
- F3 MED: monster spawns require a GM seat (403 MONSTER_SPAWN_
FORBIDDEN for non-GM unowned NPC spawns; owned-player deploys via the
service principal unaffected); encounter-builder copy matches reality.
- F4/F5/F6 LOW: viewport fog-parity claim corrected to the actual union
semantics; ability-score defaulting warns instead of silently
defaulting; .expect() calls in core replaced with error propagation;
offhand index 0 refused as matching the main hand.
All gates green: workspace tests, clippy -D warnings, pytest 496, tsc.
Co-Authored-By: Claude <[email protected]>
Closes iteration 76's disclosed zero-damage gap at the data layer:
regex extractor over canonical SRD phrasing with plausibility gates
(die-face whitelist, 13-type damage vocabulary) and warn-not-guess
paths for multi-formula, player-chosen-type, and modifier-derived
formulas. Deterministic CLI regenerates an enriched fixture (73/352
spells enriched ~20.7%; 192 genuinely have no damage; the variable-
formula pool of 58 is the largest recoverable set). Original fixture
untouched; positional pairing documented where meta entries share ids.
20 red-first tests; suite at 496.
Co-Authored-By: Claude <[email protected]>
Completes the SRD standard-action suite. Off-hand attack reuses the
normal hit math with only the damage expression swapped, positive
ability mod withheld per SRD (negative kept), Light property modeled
explicitly (opt-in on stat blocks; undeclared weapons refused, never
silently permitted), and a new spend_bonus_action enforcement point —
plus SRD ordering (Attack action must already be spent). Help stores a
consumable advantage promise ON THE BENEFICIARY, consumed exactly once
by same-side attackers through both attack routes, cleared on refresh,
and rewound with Action refund. Ledger + gateway proxies + client
helpers throughout; seed never client-forwardable. 18 red-first tests;
228 workspace / 476 pytest green, clippy -D clean.
Co-Authored-By: Claude <[email protected]>
Two workstreams sharing server.py, landed together:
Ready action (71): ReadiedAction on EntityState with Action spend via
the single enforcement point, READY_ACTION_SET ledger event, cleared on
round refresh and rewind; engine route + gateway proxy + CharacterSheet
"Holding:" display with description input. GM adjudication by design —
no automatic trigger matching. 6 red-first tests across layers.
Fourth-audit remediation (72): MED-HIGH deploy crash — Roll20 imports
persisting speed:None crashed float(None) → 500 on every deploy of such
characters; guarded fallback to 30 with math.isfinite checks and the WS
movement budget consuming the same value. LOW: out-of-range import
levels now disclose their clamp instead of silently snapping. 8
red-first tests.
Full suite: 470 passed / 22 skipped; clippy -D warnings clean.
Co-Authored-By: Claude <[email protected]>
All six engine action routes (grapple/shove/dodge/dash/disengage/
stabilize) gain gateway proxies with strict extra=forbid bodies and
identity forwarding — notably the deterministic seed is deliberately
NOT forwardable, so clients can't pin their own rolls. CharacterSheet
gains a Combat Maneuvers panel: target picker from the projected
roster, skill/effect toggles, self-buttons, downed-ally stabilize list
with an honest hint when player-role projection hides ally HP; results
quote engine fields and rejection codes verbatim with zero optimistic
mutation. 10 new proxy tests; suite at 459.
Co-Authored-By: Claude <[email protected]>
Three small audit notes closed red-first:
- Empty/[DONE]-only SSE reassembly no longer masquerades as a
completed turn: complete_with_tools raises LLM_UPSTREAM_EMPTY and the
tool agent surfaces that status instead of COMPLETED with blank
narration.
- The SLA 'intent_parsing' row is relabelled intent_parsing_keyword
(it measures regex classification); an LLM-classification row appears
only under RUN_LIVE_LLM=1 with a real key, counting only genuine llm
outcomes; SSE first-token requires >=3 samples or WITHHELD.
- Report ends with explicit FAILED/PASSED/WITHHELD tallies and an
all-withheld run exits nonzero — withheld can never read green.
21 red-first tests; suite at 451.
Co-Authored-By: Claude <[email protected]>
Third-audit web finding: two individually-clean modules broke at the
handoff — unparsable speed strings passed through unwarned and the
gateway fabricated speed=30; missing class/race/background/alignment
silently became "fighter/Human/Soldier" persisted as fact under a
docstring claiming every substitution was pre-warned. Now: unparsable
speed warns and persists None (never invented gameplay data); identity
fields persist neutral-empty with per-field warnings naming exactly
what was absent; missing level defaults to 1 WITH a warning; the
missing-core dedup no longer lets garbage-value warnings suppress
missing-stat notices. 7 red-first tests.
Co-Authored-By: Claude <[email protected]>
- P0: pytest was RED in any clean environment — dead skip guards fired
only after ConnectError had already escaped. Both helpers now skip
properly when the engine is down; suite green with no live engine.
- HIGH: starter-adventure export required ?token= while the wizard sent
an Authorization header — every click was a guaranteed 422 under a
feature claiming to work. Route migrated to _require_auth
(header-first); tests pin header-auth success and 401 semantics.
- MED: clipboard 'Copied!' claimed success unconditionally; now awaits
the write and shows an honest Ctrl+C fallback on rejection or missing
Clipboard API.
Co-Authored-By: Claude <[email protected]>
GOALS.md's latency targets were removed from the docs as unverifiable;
now they're measured. measure_slas.sh drives 200 real network calls
per category against live services and prints p50/p95/max vs targets
with a documented +5ms transport allowance: rules 0.39ms p50 (≤10ms),
spatial LoS/path 0.35/0.41ms (≤15ms), intent parsing 1.10ms (≤150ms),
SSE first-token 1.86ms (≤1200ms) — all PASS with wide margin.
Unreachable services WITHHOLD rows rather than simulating; rate-limit
429s surface as visible exclusions; sub-30-sample categories withhold
rather than judge rump samples.
Co-Authored-By: Claude <[email protected]>
The live suite proved llm.heretek.one answers text/event-stream even
to non-streaming requests, silently breaking complete_json /
generate_narrative / complete_with_tools via resp.json() failures. All
three now detect SSE content-type and reassemble assistant messages
from delta frames (tool_calls merged by index), feeding the unchanged
JSON extraction; JSONL records carry an honest transport marker;
malformed/empty streams degrade without guessing. Narrative blank-
stream degrades with degraded:true rather than empty-but-healthy.
11 red-first tests; suite at 436.
Co-Authored-By: Claude <[email protected]>
pytest 'live' marker + conftest auto-skip unless LLM_KEY is set AND
RUN_LIVE_LLM=1 — structurally CI-proof. Six tests exercise the REAL
endpoint: mechanical/safety classification provenance, Concordia
generator-marker-vs-reality (a template marker while the endpoint is
alive FAILS), strict-JSON parsing, soft 60s latency budget, and a gate
proving non-mock configuration. Connection failures degrade to SKIP.
First live run surfaced two real integration bugs, reported honestly
as skips rather than fabricated passes: .env named a nonexistent model
that hangs instead of 404ing (fixed in .env), and the endpoint returns
text/event-stream even for non-streaming calls which breaks resp.json()
(fix iteration pending). Default suite unchanged: 406 passed / 8 skips.
Co-Authored-By: Claude <[email protected]>
GOALS.md Pillar 1's drop-in AI party members: tank / skirmisher /
healer roles execute coherent turns through the same authenticated
proxy path as campaign_sim (no second HTTP stack), with optional LLM
targeting refinement validated against known entities and per-turn
provenance accounting (decision_source, fallback_reason, degraded).
Honest realizability matrix: ally-heal triage from a plain player seat
is REFUSED rather than fabricated because the role projection redacts
other PCs' HP — the companion falls back to tank behavior instead.
19 red-first tests; suite at 425.
Co-Authored-By: Claude <[email protected]>
POST /api/v1/import/roll20 accepts single or campaign exports (2 MiB
bound), maps through the importer onto the existing character storage
path so imports are owned by the caller and owner-scoped everywhere;
importer ValueErrors map to 422 with the fail-loud reason, all-
malformed campaigns refuse rather than fake a 200-with-zero. Foundry
preview route is a deliberate documented 501 — directory trees need
multipart support, pinned by test so it can't regress into fake
success. 7 red-first tests; suite at 406.
Co-Authored-By: Claude <[email protected]>
Two minor audit notes closed: the gateway's sliding-window store now
sweeps stale keys past a 100k cap (mirroring the Rust twin's guard,
same sweep-on-overflow shape, injectable for tests), and NPC
interaction magnitude is bounded to (0,10] matching the disposition
engine's band-saturation scale so one call can no longer pin a stance
to an extreme. 5 new tests; suite at 373.
Co-Authored-By: Claude <[email protected]>
Second independent audit findings, fixed red-first:
- HIGH: /engine/session-state required no token and returned FULL
engine state verbatim via a service-principal fallback. The route
now requires valid auth (401 otherwise) and no code path returns
anonymous full state.
- MED-HIGH: ledger events bypassed role projection, leaking HP/damage
numbers for hidden entities while replay export redacted them. Both
now share one projection policy (fail-closed summaries for
spectators/unknown roles).
- MED: GET /characters/{id} had an IDOR — any authenticated user could
read any character; now owner-scoped with 404 for foreign/garbage ids
(no existence oracle).
Six remediation tests verified failing before each fix.
Co-Authored-By: Claude <[email protected]>
Pillar 10 interop grounded in live schema research (foundryvtt.com
manifest docs, foundry-vtt-types, NDJSON export format): module.json +
packs parsing onto AetherTable canon via explicit per-entry mapping
tables, unmapped fields accounted per entry rather than dropped or
guessed, LevelDB-format pack dirs detected and skipped, fail-loud
ValueErrors on manifest problems, deterministic sorted output.
Roll20 guarded out of scope pending its own fixture corpus. 25
red-first tests; suite at 363.
Co-Authored-By: Claude <[email protected]>
Pillar 5 sub-agents become reachable: GET /api/v1/npc/ lists public
persona metadata; POST /npc/{id}/respond returns norms-gated
{reply, generator, stance} (taboo-violating LLM text degrades to the
template fallback with an honest norm_rejected marker); POST
/npc/{id}/interactions shifts persistent stances through the shared
disposition engine. Three personas bound to starter-adventure lore
(Drowned Steward, Baron Vane, Cabal Emissary) with distinct goals,
norms, and voices on a deterministic shared clock. 13 red-first tests;
suite at 338.
Co-Authored-By: Claude <[email protected]>
Two features sharing server.py, landed together:
Initiative order end-to-end (5.6): vtt-core begin/end_combat roll
d20+DEX initiative with deterministic seeded order (total desc, DEX
desc, name asc), COMBAT_BEGAN/ENDED ledger events; engine routes
/sessions/{id}/combat/{begin,end} with RBAC; gateway proxies forward
caller identity; the HUD renders the real rolled order with an honest
"Combat not started" empty state and GM controls. 7 core unit tests +
4 integration + mocked proxy tests.
Role-projected session state (spectator follow-up): session-state and
session-replay now project by caller role — GM/admin see everything,
players see own entities in full plus others as public board-token
projections, spectators get visible-only projections with HP/damage
amounts stripped from replay summaries; service-principal reads stay
verbatim; unknown roles fail closed to spectator view. is_player/
is_dead kept in projections because campaign_sim's targeting depends
on them through this endpoint. 20 red-first tests.
Suite at 325 passed.
Co-Authored-By: Claude <[email protected]>
GOALS.md Pillar 5's dedicated NPC sub-agents: explicit Memory (bounded
episodic, relevance+recency recall), Goals (priority/satisfaction/
feasibility walk with survival fallback), Social Norms (taboo +
obligation rules), and Linguistic Style components composing a persona.
respond_to() builds structured prompts from all four components plus
live disposition stance; LLM replies that violate norms are discarded
entirely for the deterministic stance/style-shaped fallback — violating
text never reaches output. Deterministic without an LLM; honest
generator markers. 17 red-first tests; suite at 296.
Co-Authored-By: Claude <[email protected]>
Multi-tenant retrieval per GOALS.md Pillar 7: compendium spells/
monsters/magic items indexed into the qdrant_rules collection named
and dimensioned by the repo's own collections config, payload-filter
namespacing (category + system_id + tags), deterministic UUID5 point
ids, keyword indexes on filter fields — all over raw httpx REST so the
transport stays trivially mockable. Embedding is a documented
lexical-hash embedder (MD5-bucketed, L2-normalized), not semantic;
real models are a single-function swap. lore-lookup gains semantic=true
with provenance retrieval: qdrant | substring | substring_fallback,
zero network when disabled, one honest warning when unreachable,
read-path degradation only (canon-write strictness unchanged).
29 red-first tests; suite at 279.
Co-Authored-By: Claude <[email protected]>
GOALS.md Pillar 2's first out-of-the-box module: three encounters
referencing ONLY real srd_5_2_monsters stat blocks (unknown ids raise
at definition time, so invented stats cannot ship), deterministic
seeded layout resolved via the engine's live WFC with a documented
in-module fallback and provenance recorded in the artifact, crypt-theme
loot name references, lore seeds reusing exact canon node ids, X-card-
safe content note. Ships as a real .vttbundle through the packager
(new backward-compatible adventure.json member) behind GET/POST
/api/v1/adventures/starter routes. 12 red-first tests; suite at 250.
Co-Authored-By: Claude <[email protected]>
Two features sharing server.py, landed together:
Auth headers: new client auth_headers.ts migrates lobby/character/
campaign REST calls to Authorization Bearer headers (five POSTs
previously sent NO credential at all); gateway gains a _require_auth
dependency reading header-first with ?token= fallback on migrated
routes (missing token is now 401, not 422). WS query tokens unchanged.
7 new tests.
Neo4j lore: Neo4jEpistemicGraph speaks to the dockerized neo4j:5 HTTP
transactional endpoint over httpx (zero new deps), same public surface
as the in-memory manager, all three paradox checks in one tx round
trip, tier weights matching canon, startup-only selection with honest
in-memory fallback logging, request-time failures raise rather than
silently splitting canon, predicates validated UPPER_SNAKE_CASE before
Cypher assembly. 24 tests incl. in-memory parity cases.
Co-Authored-By: Claude <[email protected]>
GET /api/v1/engine/session-replay projects the live engine ledger into
a downloadable artifact: events reduced to {sequence_id, actor_id,
event_type, is_reverted, summary} with human-readable summaries derived
only from fields actually present — partial payloads render what
exists, unknown event types pass through verbatim, reverted events are
flagged not dropped. Authenticated via forwarded caller identity;
Content-Disposition attachment naming falls back to -unknown.json when
no combat round exists. 15 red-first tests.
Co-Authored-By: Claude <[email protected]>
NpcDispositionEngine wired into CampaignSimulation: accepted attacks
record 'attacked' on the target's disposition toward the attacker
(rejected actions and non-attacks record nothing), decision prompts and
scripted-policy inputs receive each visible target's current stance
toward the acting player, and reports carry per-player end-of-run
stances plus interaction totals. All disposition timestamps use an
explicit sim clock — deterministic across identical runs; the engine's
clock is pinned so wall-clock can never fire. 'aided' hook deliberately
absent (no heal action reachable through proxies) and documented.
8 new tests; suite at 192 passed.
Co-Authored-By: Claude <[email protected]>
httpx transport exceptions stringify to "" so the JSONL audit log
recorded status:null + error:"" — indistinguishable from nothing.
Error fields now always carry the exception type. Recorded evidence
from the first LIVE campaign-simulation run against the configured
custom endpoint (llm.heretek.one): endpoint currently unreachable
from this network; the harness degraded per-turn exactly as designed,
counted every attempt, and the engine accepted 100% of executed
actions.
Co-Authored-By: Claude <[email protected]>
The deferred flagship: synthetic players authenticate, host/join/launch
a lobby, deploy RBAC-owned characters, and take turns through the same
authenticated engine proxies real clients use. Decisions come from the
configured custom LLM endpoint via complete_json with strict decision
validation (unknown actions / malformed JSON / bad coordinates fall
back per-turn while still counting the attempt); no key means honest
scripted mode with zero network. Every LLM call flows through the
existing gateway so logs/llm_calls.jsonl captures it. Reports carry
only counted metrics with enforced invariants (accepted+rejected ==
attempted). 13 tests, fully mocked; __main__ entry runs live from .env.
Co-Authored-By: Claude <[email protected]>
Audit follow-up: classify_with_llm existed only as tested library.
execute_orchestrator_turn now classifies FIRST — keyword safety hits
short-circuit before any network call or grounding; the LLM path
engages only when VTT_LLM_CLASSIFIER != 0 and a key is configured,
falling back to keywords with provenance. Non-mechanical intents are
gated away from the engine cycle (SKIPPED_NON_MECHANICAL / SAFETY_
INTERVENTION statuses), matching the playtest harness semantics.
Responses gain classifier/classified_intent/fallback_reason fields;
unconfigured environments see zero behavior change. 8 new tests with
network blocked by an exploding httpx fake.
Co-Authored-By: Claude <[email protected]>
The browser had no readable path to authoritative post-rewind state,
so local tokens drifted until next hydration. The x-card response now
carries a full serialized GameSession snapshot taken after the rewind
(Null-degrading on serialization failure), and the gateway gains POST
/api/v1/engine/session-state — the first browser-readable proxy to
authoritative engine state, with identity forwarding when a token is
present. Per-role visibility filtering documented as future work.
8 pytest proxy tests + 1 integration test pinning post-heal rewind HP.
Co-Authored-By: Claude <[email protected]>
Replaces greedy first-fit with STRIPS-style forward search: actions
declare preconditions and effects (thresholds + delta wrappers), the
planner runs uniform-cost A* over world-state snapshots with
deterministic tie-breaking, dominance dedup, depth/expansion caps
bounding EspionageRaid's positive resource loop. Risk folds into
planning cost; execution stays deterministic. Legacy tick behavior is
preserved verbatim when no goal is set — same three actions, same log
lines — so existing callers are untouched. New API: set_goal/clear_
goal/plan/execute_plan/goal_progress_pending. 19 new tests including
optimality-over-greedy and legacy-equivalence pins.
Co-Authored-By: Claude <[email protected]>
POST /api/v1/engine/heal and /api/v1/engine/rest follow the established
proxy pattern: required token Query param, _caller_actor forwarded so
engine RBAC authorizes the real caller, strict Pydantic models
(extra=forbid, Literal rest kind) mirroring the engine's
deny_unknown_fields contract so no client math crosses the boundary.
8 new proxy-identity tests; suite at 150 passed.
Co-Authored-By: Claude <[email protected]>
classify_with_llm() prompts the configured custom endpoint
(LLM_API/LLM_KEY/LLM_MODEL) for JSON classification into the real
IntentType enum, with layered honesty and safety:
- SAFETY_INTERVENTION always wins — keyword safety short-circuits
before any network call; an LLM safety label overrides keyword
disagreement
- any failure (kill-switch VTT_LLM_CLASSIFIER=0, no key, malformed or
unknown output) falls back to the deterministic keyword classifier
with provenance fields; never raises
- RULE_OF_COOL has no enum counterpart and is deliberately not mapped
complete_json() adds fence-tolerant JSON extraction + non-streaming
chat-completions with response_format json_object; degraded-marker
code preserved. 12 new tests, HTTP fully mocked.
Co-Authored-By: Claude <[email protected]>
When the LLM upstream fails (no key, non-200, mid-stream error), the
deterministic fallback generator now announces itself: a sentinel from
stream_narrative makes the gateway emit a leading {degraded, reason}
frame and tag every subsequent frame degraded:true. Healthy streams
carry no degraded key at all. Non-streaming generation gains
generate_narrative() returning {narrative, degraded, reason?} with
shared canned prose so both paths stay identical. Clients can finally
tell real LLM narration from canned fallback.
Co-Authored-By: Claude <[email protected]>
Continuous directed relationship scores per (npc, player) pair per
GOALS.md Pillar 8: trust and fear with independent exponential decay
(fear half-life ~3.5x shorter), alignment-compatibility bias, NPC-level
stress that amplifies fear, event-driven interaction recording with
strict kind validation, stance banding (hostile..allied), and lazy
decay computed at query time so scores are pure functions of recorded
events. Injectable clock keeps it deterministic for replay/testing.
Pydantic v2 record model matches project conventions. 15 new tests.
Co-Authored-By: Claude <[email protected]>
AFPR's rejection branch was mathematically unreachable (run_turn_cycle
only returns COMMITTED / FALLBACK_COMMITTED), so false positives were
tautologically 0%. Now:
- grounded proposals that get rejected ARE counted as auditor false
positives (a grounded draft describes the genuine engine outcome);
- ~10% of audited turns inject a recall probe — a draft narrating the
death of a target live state says is alive — which the auditor MUST
catch; new auditor_recall_pct metric (target >= 95%) reported by the
benchmark suite;
- entity counts fed to the audit come from the live session snapshot.
Live run: MCR 100%, HCI 1.0, AFPR 0.0%, recall 16/16 = 100%.
Co-Authored-By: Claude <[email protected]>
- Grounding: NarrativeGenerateRequest gains engine_session_id /
target_entity_id. When a session is named, _resolve_audit_inputs fetches
the live snapshot and the auditor reconciles against IT: lethality fields
come from the live entity (a payload claiming death while the engine says
25 HP can no longer smuggle a kill past Vector 3), entity counts come
from the live snapshot, named-but-missing targets are rejected as ghost
entities, and an unreachable engine fails closed with
WORLD_INSPECTOR_UNAVAILABLE instead of auditing stale client claims.
- Streaming: tokens are now HELD until the sentence they belong to has
passed the auditor; violating sentences and everything after them are
never emitted — only the corrective SYSTEM frame and the cut.
- Lore continuity: Vector 4 was dead code (nothing ever populated its
lore_* payload keys). The auditor now derives (subject, predicate,
object) triples from canon entity names and predicate verbs appearing in
the draft itself, including reflexive assertions like "Oakhaven Keep
still stands". Payload triples still take precedence.
Co-Authored-By: Claude <[email protected]>
Unparseable input used to silently yield a combat-ready fake: AC 14,
HP 30, invented "Claw Strike" action with +5 / 2d6+3. Core fields now
stay absent when not parsed, every missing field is reported in
warnings, and responses carry parse_ok. Strict mode (new flag on
HomebrewParseRequest) rejects with 422 instead of returning a
half-invented statblock. Placeholder lists are copied per call to stop
cross-parse state leaks.
Co-Authored-By: Claude <[email protected]>