GET /sessions/{id}/replay/export?format=json|markdown: GM/admin get
verbatim events; players get summary-only projection (no raw payloads or
state hashes ride along); spectators fail closed 403. Markdown renders
round-by-round with X-card rewind markers and honest truncation at 5000
events. 25 tests red-first; pytest 885 passed / 28 skipped.
Co-Authored-By: Claude <[email protected]>
Benchmark boot with a LIVE engine exposed EngineRejectedError (404 unknown
session) escaping the x-card handler as a hard failure — the orchestrator-
side intervention must still record. Engine rewind is best-effort: rejection
now surfaces as engine_rewind.status=ENGINE_REJECTED with the real code.
Proven against a booted engine; full suite green.
Co-Authored-By: Claude <[email protected]>
Autosave snapshot carries quest-graph and faction-state slots on the
existing campaign_saves JSONB; explicit POST /campaign/restore (GM-only,
ownership-scoped) reapplies them; readonly quest reads self-heal from the
newest owned save instead of rolling new canon. Corrupt slots → honest
422 *_CORRUPT; legacy saves restore cleanly. Inventory of memory-only
singletons audited and documented durable-vs-local. 9 tests red-first;
pytest 839 passed / 27 skipped.
Co-Authored-By: Claude <[email protected]>
Closes A3 #5/#6: anonymous/garbage-token callers resolve the default
corpus with zero engine round trips (junk-uuid amplification closed);
deterministic resolutions cached 30s/256 entries. 404 vs unreachable vs
missing-version now produce distinct provenance statuses with the real
HTTP status surfaced. 16 tests red-first; pytest 830 passed/27 skipped.
Co-Authored-By: Claude <[email protected]>
Closes A3 #1/#2: one shared extraction budget draws down across all
entries (mid-copy cutoff), declared-size sum refused pre-extraction,
chunked streaming upload caps memory on lying Content-Length; manifest
pack paths contained via commonpath with skip-with-warning. Lying
comments corrected. 8 tests red-first incl. a lying-reader seam for
the zipfile declared-size cap; pytest 812 passed / 29 skipped.
Co-Authored-By: Claude <[email protected]>
Both SRD editions load at startup when fixtures exist; compendium and
stream-grounding routes resolve the session's rule_version from the
engine snapshot and serve the matching corpus with additive provenance
(rule_version/source/reason). Version-mismatched RAG queries honestly
degrade to versioned substring scan instead of mislabeled semantic hits.
14 tests red-first; pytest 796 passed / 27 skipped.
Co-Authored-By: Claude <[email protected]>
Sorted-set sliding windows keyed per bucket+identity when REDIS_URL is
set and reachable; rejected attempts still cost nothing and Retry-After
matches the memory backend value-for-value. Availability-first: any Redis
failure flips one permanent degraded flag with a single honest warning,
then delegates to the shared memory backend. Two-replica budget-sharing
test proves the quota-multiplication defect gone. 12 tests red-first;
pytest 776 passed / 26 skipped.
Co-Authored-By: Claude <[email protected]>
SyntheticPlaytestRunner now takes a seed (default 42) driving utterance
mix + 10% recall-probe injection via random.Random(seed) — benchmark
deltas are reproducible across runs instead of depending on global
RNG state.
Co-Authored-By: Claude <[email protected]>
Live checkpoint vs llm.heretek.one found the gateway deterministically
degrades when a raw single quote reaches it — the router prompt's
'player's' flipped 'I attack the goblin' from MECHANICAL_INVOCATION
(0.95) to IN_CHARACTER_DIALOGUE (0.72). Prompt reworded, live suite
6/6 green. Root cause is upstream template/escaping; documented at
the call site.
Co-Authored-By: Claude <[email protected]>
Replaces hash pseudo-embeddings with BAAI/bge-small-en-v1.5 dense +
Qdrant/bm25 sparse legs fused by reciprocal-rank fusion over Qdrant
named vectors. Opt-in via QDRANT_EMBEDDINGS=1; offline fallback keeps
hash embedder relabeled qdrant-hash-fallback with an honest warning.
Live-verified semantically on a compendium subset. 27 new offline-safe
tests; pytest 713 passed / 25 skipped.
Co-Authored-By: Claude <[email protected]>
Closes audit F10 at the gateway: ac 0..30, hp 0..1000, finite speed,
typed spell-slot dict; damage expressions validated against a strict
dice grammar (sides/dice/constant caps) since the engine rolls spawned
expressions unclamped. ingress is now a strict EngineIngressEvent model
matching the engine's SCREAMING_SNAKE_CASE wire format. 18 tests
red-first; pytest 690 passed / 21 skipped. All SRD corpus callers pass.
Co-Authored-By: Claude <[email protected]>
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]>