Commit Graph
87 Commits
Author SHA1 Message Date
John SmithandClaude 87343b653a feat(gateway): death-save audit in replay export (iteration 57)
Optional include=death_audit flag on the replay export derives
trigger→save-attempts→outcome episodes per token from ledger events
(rewound events excluded, truncation scoped honestly). Instant deaths,
stabilizations, and in-progress recoveries all surfaced; empty ledgers
admit unavailability instead of inventing sections. 12 tests red-first;
pytest 897 passed / 28 skipped.

Co-Authored-By: Claude <[email protected]>
2026-08-25 15:49:12 -04:00
John SmithandClaude 908504e4f3 feat(gateway): session replay export, role-projected (iteration 55)
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]>
2026-08-25 15:31:49 -04:00
John SmithandClaude 47ca2bd3c2 feat(content): Sunken Crypt of Karas deepened to playable campaign shape (iteration 52)
8-chamber 44x30 map with per-chamber engine-WFC stitching, 7-node
encounter tree with save-DC hazards, DMG XP-threshold balance audit at
import (old opener was deadly+; now 5 fights EASY/MEDIUM, wight climax),
real compendium item ids in milestone loot, 9 tiered paradox-checked
lore seeds, full export→import round-trip verified. 22 tests red-first;
pytest 860 passed / 28 skipped.

Co-Authored-By: Claude <[email protected]>
2026-08-25 14:02:19 -04:00
John SmithandClaude 0d6393e690 fix(gateway): x-card survives live-engine rejection honestly (milestone gate fix)
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]>
2026-08-25 11:20:53 -04:00
John SmithandClaude 6b43e7d90c feat(gateway): persistent campaign quest + faction state across restarts (iteration 47)
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]>
2026-08-25 11:03:17 -04:00
John SmithandClaude 7d23fbfd19 fix(gateway): authenticated session-scoped corpora, TTL cache, honest statuses (iteration 44)
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]>
2026-08-25 10:37:36 -04:00
John SmithandClaude 31a0e1783a fix(gateway): cumulative zip budget + importer path containment (iteration 42)
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]>
2026-08-25 10:26:24 -04:00
John SmithandClaude a6af1c91e0 feat(gateway): Foundry module zip upload with hardening (iteration 40)
Replaces the multipart-transport 501: gm/admin-gated upload, zip-slip/
absolute-path/symlink rejection, double-enforced size+expansion caps
(413), manifest-root discovery delegating to the fail-loud importer.
501 reasons list updated to only still-true limitations. 10 tests
red-first; pytest 806 passed / 27 skipped.

Co-Authored-By: Claude <[email protected]>
2026-08-25 09:32:35 -04:00
John SmithandClaude 04f9e318fc feat(gateway): per-session rule-version corpus branching (iteration 37)
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]>
2026-08-25 09:14:29 -04:00
John SmithandClaude 716c80c8dc feat(gateway): lobby ready flags + character binding + launch gating (iteration 33)
Per-member ready/selected_character_id persisted in MemoryStore and
Postgres (idempotent ALTER migration); member-scoped ready/character
routes with ownership validation; host launch refuses unready members
(409 with honest roster) unless host forces. Campaign sim readies seats
before launch. 7 new tests red-first; pytest 782 passed / 27 skipped.

Co-Authored-By: Claude <[email protected]>
2026-08-25 05:17:38 -04:00
John SmithandClaude 2812c8f3f9 feat(gateway): Redis-backed distributed rate-limit windows (iteration 31)
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]>
2026-08-25 04:26:07 -04:00
John SmithandClaude ccdf74271d feat(compendium): spell damage coverage 73→82/352 with upcast metadata (iteration 27)
Parser handles conjunctive one-hit components, modifier-derived
formulas (with explicit disclosure), exact flat values, and slot-scaling
rows — 45 spells gain structured upcast metadata. Distinct-trigger and
choice-dependent formulas stay honestly warned per the no-guessing
contract; coverage regression below 73 now fails CI. 17 tests red-first;
pytest 764 passed / 26 skipped.

Co-Authored-By: Claude <[email protected]>
2026-08-25 02:07:34 -04:00
John SmithandClaude 5551a321b7 feat(gateway): DirectorAgent wired to real campaign-sim tension signals (iteration 24)
Deterministic tension tracker: weighted components (hp swing/deaths/
disposition shifts/round pressure/quest stage) fold into exponential
smoothing over the sim's round loop; recommendations (raise_stakes,
introduce_complication, spotlight_player) derive from pure thresholds.
LLM only phrases hook text with provenance labels. Sim report gains a
director telemetry block; identical runs byte-identical. 35 tests
red-first; pytest 747 passed / 26 skipped.

Co-Authored-By: Claude <[email protected]>
2026-08-25 01:13:50 -04:00
John SmithandClaude dfa97ce58a fix(playtest): seeded RNG for reproducible benchmark sampling (iteration 22)
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]>
2026-08-25 00:53:29 -04:00
John SmithandClaude 024c140d0a fix(routing): apostrophe-free classifier prompt after live-LLM probe (iteration 21)
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]>
2026-08-25 00:32:43 -04:00
John SmithandClaude 5f47c5a8dc feat(gateway): real hybrid dense+sparse RAG via fastembed (iteration 18)
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]>
2026-08-25 00:24:15 -04:00
John SmithandClaude 8123d6e216 refactor(gateway): delete dead fabricating modules (iteration 16)
Removed AstPdfCompendiumParser (fabricated ac=10/hp=10 defaults),
LiteLLMCircuitBreakerGateway (canned cloud-call simulation),
VttBundleBridge (weaker duplicate exporter + fabricated Foundry
defaults), ConcordiaNPCComponent (template chatter). Import graph
cleaned; live fail-loud paths (HomebrewMarkdownParser, FoundryModuleImporter,
LLMStreamingGateway, ConcordiaNPC) untouched. Suite unchanged at 690.

Co-Authored-By: Claude <[email protected]>
2026-08-24 23:05:44 -04:00
John SmithandClaude 312ab23682 feat(gateway): magnitude bounds, damage grammar, typed ingress on spawn (iteration 15)
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]>
2026-08-24 22:49:50 -04:00
John SmithandClaude 4e8477d08b feat(gateway): admin bootstrap allowlist, fail-closed secret, lobby membership gate (iteration 12)
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]>
2026-08-24 21:14:10 -04:00
John SmithandClaude fa18ebfff8 feat(gateway,client): auth + spend buckets on agent/dynasty/simulation/LLM routes (iteration 10)
Closes audit A1 blockers F1-F4: /agent/turn gated to GM or bound-session
participants; dynasty inject-lore and simulation tick GM-only; empirical-
benchmark GM-only with 5/min bucket; intent/orchestrator/narrative routes
authenticated in a 30/min llm bucket. Client stream/dynasty call sites send
credentials with honest sign-in gating. Compose now passes overridable
VITE_YSYNC_WS_URL to vtt-client. 33 red-first pytest tests, 6 vitest;
pytest 651 passed / 22 skipped, vitest 102.

Co-Authored-By: Claude <[email protected]>
2026-08-24 20:48:36 -04:00
John SmithandClaude b7bb40b160 feat(gateway): durability-bridge auth + header-first tokens on legacy routes (iteration 8)
persist/hydrate require auth with x-card-style participation gating
(gm/admin any session, others via bound lobby roster, unbound fail closed).
All remaining token:Query routes migrated to _require_auth (Bearer preferred);
EnginePersistRequest.owner_user_id spoofing removed — ownership from verified
token. Sweep documented remaining unauthenticated LLM-spend/agent routes for a
coordinated follow-up. 11 new tests red-first; pytest 618 passed / 22 skipped.

Co-Authored-By: Claude <[email protected]>
2026-08-24 19:26:51 -04:00
John SmithandClaude f6ee77c8fc feat(gateway): epistemic ladder + x-card/spotlight authorization (iteration 5)
Lore assertions enter at SUBJECTIVE_RUMOR; GM promotes one step per call
through paradox review, players never self-promote (403 LORE_TIER_FORBIDDEN).
X-card requires token + session participation (403 X_CARD_NOT_A_PARTICIPANT);
spotlight recording validates speaker identity (403 SPOTLIGHT_SPOOFED_SPEAKER).
29 new tests red-first; pytest 607 passed / 22 skipped.

Co-Authored-By: Claude <[email protected]>
2026-08-24 19:04:53 -04:00
John SmithandClaude c1fc3dbebb feat(gateway,client): mandatory auth on every engine proxy, client sends tokens everywhere (iteration 3)
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]>
2026-08-24 18:46:58 -04:00
John SmithandClaude 3bf115ea0e feat(gateway): strict typed spawn schema blocks smuggled combat math (iteration 2)
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]>
2026-08-24 18:15:24 -04:00
John SmithandClaude c5d9cc6b37 fix(gateway): server-side auth for quest routes (iteration 94)
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]>
2026-08-24 16:34:20 -04:00
John SmithandClaude d9c3973838 feat(gateway): GM campaign autosave from live engine state (iteration 85)
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]>
2026-08-24 15:40:12 -04:00
John SmithandClaude 703f5c1e00 fix: sixth-audit remediation — gate reliability + honesty corrections (iteration 84)
- 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]>
2026-08-24 15:32:44 -04:00
John SmithandClaude 1a36cb6113 feat(gateway): role-enforced handout persistence (iteration 83)
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]>
2026-08-24 14:32:40 -04:00
John SmithandClaude dac7f36d77 feat(simulation): multi-generation dynasty lineages, alliances, prestige (iteration 82)
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]>
2026-08-24 14:18:22 -04:00
John SmithandClaude e51bb2b8c0 feat(simulation): parametrized quest generation from curated theme tables (iteration 79)
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]>
2026-08-24 13:59:24 -04:00
John SmithandClaude 7a104a7ebf feat(simulation): Concordia social-dialogue phase in campaign simulation (iteration 80)
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]>
2026-08-24 13:58:52 -04:00
John SmithandClaude 7271475539 fix(engine+client): fifth-audit remediation across six findings (iteration 78)
- 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]>
2026-08-24 13:46:36 -04:00
John SmithandClaude 96676b499f feat(compendium): conservative spell-damage extraction pipeline (iteration 77)
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]>
2026-08-24 12:58:51 -04:00
John SmithandClaude d62cc5dade feat(core+engine): two-weapon fighting and Help action (iteration 74)
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]>
2026-08-24 12:30:47 -04:00
John SmithandClaude 511c6271a4 feat+fix: Ready action end-to-end; deploy crash + clamp disclosure (iterations 71-72)
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]>
2026-08-24 08:55:52 -04:00
John SmithandClaude 133d15b1ad feat: combat maneuvers reachable end-to-end (iteration 71)
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]>
2026-08-24 08:32:15 -04:00
John SmithandClaude de7d011175 fix(llm+sla): empty-stream honesty; SLA rows disclose what they measure (iteration 70)
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]>
2026-08-24 08:25:37 -04:00
John SmithandClaude 682228cc68 fix(import): Roll20 honesty seam between importer and gateway (iteration 67)
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]>
2026-08-24 07:58:16 -04:00
John SmithandClaude c3b3f78535 fix: third-audit remediation — gate integrity, wizard export, clipboard honesty (iteration 65)
- 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]>
2026-08-24 07:50:36 -04:00
John SmithandClaude 1409e3b2de feat(playtest): SLA measurement harness — honest latency tables (iteration 62)
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]>
2026-08-24 07:09:21 -04:00
John SmithandClaude bfe327380c fix(llm): SSE-tolerant non-streaming calls for streaming-only proxies (iteration 56)
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]>
2026-08-24 06:27:49 -04:00
John SmithandClaude 30850a5e59 test(llm): opt-in live-LLM suite that never runs in CI (iteration 55)
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]>
2026-08-24 06:24:12 -04:00
John SmithandClaude fdca883144 feat(simulation): autonomous AI companion PCs with tactical roles (iteration 54)
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]>
2026-08-24 06:22:25 -04:00
John SmithandClaude db0f8a137e feat(gateway): Roll20 import endpoint with owned persistence (iteration 51)
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]>
2026-08-24 06:07:20 -04:00
John SmithandClaude 24ead361b2 feat(compendium): Roll20 character importer (iteration 50)
Pillar 10's second interop path, grounded in researched sources
(Roll20 API object docs, real exporter payload, 5e OGL sheet attribute
index): single-character and campaign-envelope imports, explicit-mod
preference with disagreement warnings, score-from-lone-mod derivation
always warned, honest None+warning for missing core stats, unmapped
attribute accounting, first-wins duplicates, fail-loud ValueErrors,
deterministic sorted output. 26 red-first tests; suite at 399.

Co-Authored-By: Claude <[email protected]>
2026-08-24 05:54:59 -04:00
John SmithandClaude fb42d4367b fix(gateway): bounded rate windows, capped NPC interaction magnitude (iteration 48)
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]>
2026-08-24 05:50:02 -04:00
John SmithandClaude 7f592fe7a8 fix(gateway): audit remediation — session-state auth, ledger redaction, IDOR (iteration 44)
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]>
2026-08-24 05:32:46 -04:00
John SmithandClaude 6db24190e1 feat(compendium): Foundry module importer; tracker through iteration 41
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]>
2026-08-24 05:01:41 -04:00
John SmithandClaude 35de8764a9 feat(gateway): Concordia NPC dialogue endpoint with starter personas (iteration 38)
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]>
2026-08-24 04:49:44 -04:00
John SmithandClaude e65dee3e28 feat: combat lifecycle + role-projected session state (iterations 36-37)
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]>
2026-08-24 04:42:39 -04:00