325 Commits
Author SHA1 Message Date
John Smith 8068e88341 docs: Loop3 iteration 42 row (invisible + petrified condition rings) 2026-08-31 08:15:06 -04:00
John Smith 75d3a1f35e feat(client): theme invisible + petrified condition rings (loop 3, iteration 42)
The condition-ring module (iteration 25) deliberately dropped invisible and petrified as 'engine-only' — but the engine now wires both mechanically (iteration 41: Invisible grants advantage on its own attacks; Petrified is incapacitated, auto-fails STR/DEX saves, grants advantage to attackers). Added both to THEMED_CONDITIONS + CONDITION_RING_THEMES (invisible = cyan-300 ScanEye, 10.5:1; petrified = zinc-300 Gem, 10.3:1 — both WCAG-AA vs --tavern-bg) and to CONDITION_PRIORITY (petrified at #3 after paralyzed, invisible at #7 after blinded). Updated the 4 stale 'engine-only' doc comments (module header, themedConditions, hasThemedCondition, TacticalCanvas ring comment) and the 2 DOM tests that pinned the old drop behavior. 4 new unit tests + 2 new DOM tests — vitest 799 (was 795), tsc clean, build 4.66s.
2026-08-31 08:13:59 -04:00
John Smith 7b3aec8f60 docs: Loop3 iteration 41 row (SRD Invisible attacker-side advantage) 2026-08-31 07:44:31 -04:00
John Smith 7070dfe79f feat(engine): SRD Invisible — invisible creature's own attacks have advantage (loop 3, iteration 41)
Condition::Invisible was half-wired: the target-side clause (attacks against it have disadvantage) existed via inflicts_disadvantage_on_attacker, but the attacker-side clause (the creature's own attacks have advantage) had no path. Added Condition::grants_advantage_on_own_attacks and wired it into RulesEvaluator::edge_from_conditions alongside the target-side advantage sources. Cancellation semantics preserved: a blind+invisible attacker sets both flags and resolves as a straight d20 in resolve_attack. 3 new unit tests (own-attack advantage, symmetric target-side non-regression, blind+invisible cancellation) — cargo test --workspace 524/0 (was 523), clippy -D warnings clean.
2026-08-31 07:43:13 -04:00
John Smith 052c99cc06 docs: Loop3 iteration 40 row (SRD Charmed attack prohibition) 2026-08-29 03:46:50 -04:00
John Smith 88ad56dc7f feat(engine): SRD Charmed — a charmed creature can't attack the charmer (loop 3, iteration 40)
Charmed existed in the Condition enum with zero mechanical wiring. Automate
the one unambiguous SRD prohibition ("the creature can't attack the
charmer") on the same honesty model as the Frightened approach rule:

  - GameSession::charm_blocks_attack: true iff the attacker carries Charmed
    AND the ledger attributes that charm to the target. An unattributed charm
    (no source named, or the source has left the ledger) blocks nothing — a
    clause that names no one cannot be violated by anyone. A since-removed
    charm blocks nothing.
  - resolve_attack gate: a charm-blocked swing is refused 409
    CHARMED_CANNOT_ATTACK BEFORE any Action/ledger side effect, so the
    prohibition is pure validation, not a spent turn.

No new data fields; reuses the existing condition_source attribution + the
condition name/representative matchers, so it is rewind-consistent. 3 core
unit tests + 2 HTTP integration tests (blocked attack on charmer, legal
attack on a bystander, unattributed charm blocks nothing).

cargo test --workspace: 523 passed / 0 failed; clippy -D warnings clean.
2026-08-29 03:42:16 -04:00
John Smith f14e49a5d8 docs: correct L3.39 row — six centralized paths + engineGenerateMap follow-up 2026-08-29 00:18:48 -04:00
John Smith 79973f37a6 refactor(client): centralize engineGenerateMap rejection too (L3.39 follow-up)
The audit grep caught a sixth inline rejection path I'd missed:
engineGenerateMap in rules_engine.ts still did its own
`?.detail ?? payload` + `HTTP ${status}` parse. Route it through the shared
engineRejectionDetail, preserving the code-first ordering: a 5xx body that
names an engine code is a rejection (not a network failure), while a
code-less 5xx stays 'unreachable'. Net -14 lines; tsc clean, vitest 795,
build 4.55s.
2026-08-29 00:14:29 -04:00
John Smith 0ad0fcf676 docs: Loop3 iteration 39 row (centralized engine rejection parser)
Also reconcile L3.35 placeholder with its committed hash b661542.
2026-08-28 22:59:35 -04:00
John Smith 813a19a21d refactor(client): centralize engine rejection parser (loop 3, iteration 39)
Five engine-action modules (rules_engine, opportunity_state, combat_delay,
combat_surprise, encounter_store) each carried a near-identical local
rejectionFrom that stripped the FastAPI {detail} envelope and read only
error+message. Replace them all with the shared engine_errors.ts parser,
which is a strict superset:

  - still reads code (error|code) + message (message|detail) verbatim
  - captures the engine's human sentence when it rides `detail`, not
    `message` (local copies returned null there)
  - honest nulls for non-JSON/unrecognized bodies; the renderer supplies
    the HTTP <status> fallback instead of the parser inventing 'HTTP 4xx'
  - 422 validation arrays quote the first entry msg

14 red-first unit tests pin the honesty contract (never fabricate a code or
sentence). Client vitest 795 + relay 66 green; tsc clean; build 4.69s.
2026-08-28 22:50:16 -04:00
John SmithandClaude 316e31da0d docs: Loop3 iteration 38 row (media gateway migrated to llm.heretek.one)
Co-Authored-By: Claude <[email protected]>
2026-08-27 00:29:03 -04:00
John SmithandClaude b661542ec8 fix(gateway)+benchmark: surprise proxy wire gap; live cross-stack e2e (loop 3, iteration 35)
Milestone verification caught the exact defect class it exists for: the
client's GM Surprised toggle dials /api/v1/engine/combat/surprise but
the gateway never registered the proxy — the button 404'd in production
while the vitest mock (which stubs fetch) passed green. Adds
EngineSurpriseAdjudicationRequest + engine_combat_surprise forwarding
{entity_id, surprised} + the caller's real identity; the engine's own
RBAC still enforces GM-seat and first-round window. Regression guards:
combat-surprise in anonymous-401 cases + TestCombatProxy (missing-token
401, path+coerced-UUID payload+real-actor forwarding, extra-field 422,
unreachable-engine 502).

New engine-live module test_live_surprise_delay_timeline.py drives the
real gateway against the real engine (skips when engine down, matching
repo convention): surprise grant through the proxy, hidden-NPC surprise
not leaked to a player feed, ENTITY_SURPRISED delay refusal, surprise
release after first turns, delayed-slot skip, spotlight report +
SPOTLIGHT_SPOOFED_SEAT denial, timeline surfacing SURPRISE_GRANTED/
RELEASED + DELAY_TAKEN with no hidden name in the player feed.

Benchmark run: all gates PASS — playtest MCR 100%, HCI 1.0, AFPR 0.0%,
auditor recall 14/14; cargo 518; pytest 1350/34 standalone (+3 guards),
1366/18 engine-up; client 768/58 + relay 66.

Co-Authored-By: Claude <[email protected]>
2026-08-26 17:24:30 -04:00
John SmithandClaude e0c5f30643 docs: Loop3 tracker row for iteration 34
Co-Authored-By: Claude <[email protected]>
2026-08-26 17:12:18 -04:00
John SmithandClaude 0e799e7c07 feat(client): Surprise round UI mirroring Delay (loop 3, iteration 34)
Toolbar Surprised (Zap, amber/red) badge distinct from the Delayed
(Hourglass, sky) badge, plus GM mark/clear buttons beside Delay/Resume
gated on the same controlledEntityIds seam (engine re-verifies GM and
round window). combat_surprise.ts dials /api/v1/engine/combat/surprise
{session_id, entity_id, surprised}; engine codes verbatim
(SURPRISE_WINDOW_CLOSED, FORBIDDEN_ROLE, ENTITY_DELAYED, NOT_IN_COMBAT).
combat.surprised flows through App's snapshot carry verbatim (server
projection is the only authority; rendered ids never re-filtered
client-side). NOTE: gateway does not proxy /combat/surprise yet — the
client dials ahead of the gateway exactly as Delay did in iteration 21;
a python lane will add the proxy.

Gates: vitest 781 passed / 58 files (+30); relay 66; build clean in 4.9s.

Co-Authored-By: Claude <[email protected]>
2026-08-26 17:12:04 -04:00
John SmithandClaude 0f6ddd3cd2 docs: Loop3 tracker row for iteration 32
Co-Authored-By: Claude <[email protected]>
2026-08-26 17:02:32 -04:00
John SmithandClaude 3aaecc42d4 feat(engine): SRD 5.1 Surprise round with rewind-consistent skip (loop 3, iteration 32)
Survey confirmed delayed" is the cleanest analog, so surprise is a
per-combat surprised: Vec<Uuid> on InitiativeCombatState (serde-default,
legacy-safe) modeled as skip-and-release inside advance_round's bounded
cursor scan rather than an entity-local no-budget flag — reuses the
delayed machinery verbatim for rewind, epoch-scoping, ghost-prune, and
non-GM hidden-id projection. Mechanics: surprised can't move/act, and
reaction is disabled at grant and re-disabled after every wholesale
budget reset while still surprised; each concluded first turn releases
(SURPRISE_RELEASED journaled). begin_combat_with_surprise + optional-
body begin route; POST /combat/surprise gm/admin-only first-round
window (403/404/409/422). Flags kept orthogonal: take_delay on a
surprised creature -> ENTITY_SURPRISED, grant on delayed ->
ENTITY_DELAYED.

Gates: cargo test --workspace 518 passed / 21 suites (baseline 504,
+14); clippy clean.

Co-Authored-By: Claude <[email protected]>
2026-08-26 17:02:18 -04:00
John SmithandClaude 9ed80954b7 docs: Loop3 tracker row for iteration 33
Co-Authored-By: Claude <[email protected]>
2026-08-26 16:59:23 -04:00
John SmithandClaude 4c1196b17d feat(gateway): session spotlight aggregation over self-reports (loop 3, iteration 33)
POST /sessions/{id}/spotlight/report accepts participant self-reports of
local speaking (0..600000ms, member seat only, per-IP tight spotlight
bucket); GET /sessions/{id}/spotlight returns per-roster-seat rolling
scores decayed at half-life 3min with is_quiet = score < mean - sigma
and an observed_at staleness stamp; DELETE clears scores (GM/admin).

Honesty contract: survey confirmed the CRDT speech ledger is real and
remote-aware at the client layer, but python has no read-path into the
Y.Doc (Node relay), so the self-report channel is the honest seam this
iteration can implement; every route states that nonzero values
originate from spoofable client self-reports (insight tooling for the
DM, not an authentication boundary). DirectorAgent remains unwired by
design — aggregation is built so a future iteration can feed it. The
legacy VoiceSpotlightTracker hardcoded demo roster is left untouched.

Gates: pytest 1347 passed / 33 skipped (baseline 1302, +45).

Co-Authored-By: Claude <[email protected]>
2026-08-26 16:59:11 -04:00
John SmithandClaude 623d60e23f docs: Loop3 tracker rows for iterations 27, 30, and 31
Co-Authored-By: Claude <[email protected]>
2026-08-26 16:46:16 -04:00
John SmithandClaude d3f5130ba0 fix(engine): Exhaustion tuple-variant clocks survive rewind replay (loop 3, iteration 30)
F4 from audit 2. Externally-tagged serde renders the enum's only tuple
variant Condition::Exhaustion(u8) as an object {"exhaustion": <level>}
whose .as_str() is None, so TWO F6 sites silently skipped it: the
CONDITION_APPLIED rebuild never re-armed an exhaustion timer (journal-
invisible), and the TURN_ADVANCED name_match never restored recorded
remaining_rounds (rewinding snapped an exhausted character back to its
pre-decrement full level). Repaired both — the reproduction test first
failed with left:4 right:3 (the literal symptom), green after the
name_match fix. Branches documented: legacy payload lacking the field
matches nothing; a differing snapshot level cross-checks level equality;
the payload's recorded level is authoritative.

Hardening survey: the documented F6 residual (conditions expired before
the rewind point resurrect from the CONDITION_APPLIED-only rebuild)
confirmed empirically, and deliberately NOT force-fixed — the snapshot
writer omits zero-clock entities and within-entity absent conditions, so
an alive-set prune would be lossy; the exact limitation is documented
in the F6 comment block for a future writer-side change.

(Also rides along the purge lane's verify_entity_conservation removal,
which shared this file with iteration 27.)

Gates: cargo test --workspace 504 passed / 21 suites; clippy clean.

Co-Authored-By: Claude <[email protected]>
2026-08-26 16:46:05 -04:00
John SmithandClaude d788f4ba14 refactor(engine): measurement-honest dead-code purge (loop 3, iteration 27)
Survey-driven removal of 285 lines across 10 crates files, only items
grep-proven to have zero callers in any language (crates/, python/,
client/, scripts/): calculate_rule_of_cool_dc, calculate_plunge_attack_
bonus, add_modifier + MulticlassSpellSlotMatrix + SpellcastingStats
(modifier_graph), remove_item / check_encumbrance / reveal_curse +
EncumbranceStatus (inventory), replay_session_state, render_generation
apologetics, user_connection_count, DeathSaveActionReq, Vector3
manhattan/chebyshev/normalize/dot + AreaOfEffect::contains_point
(spatial), generate_room_dressing + DungeonRoomDressing (wfc), plus
chain-dead callers. Re-export list pruned in lib.rs.

Keeps-with-reason documented: wire-schema serialization fields (even
readerless), per-user WS-cap writers, test fixtures. New regression
guard test_file pins 45 wire-format exports present + 3 purged stay
absent (compile-time sentinel import block).

Note: state.rs (verify_entity_conservation removal) shared a file with
the parallel iteration-30 lane's in-flight F4 rewind work and will land
in that commit.

Gates: cargo test --workspace 503 passed / 21 suites; clippy clean.

Co-Authored-By: Claude <[email protected]>
2026-08-26 16:39:50 -04:00
John SmithandClaude ab220ca58e feat(client): 3D-positioned tag SFX + local VAD spotlight meter (loop 3, iteration 31)
Task A: token right-click tag cue routed through the existing spatial
audio chain (HRTF or StereoPanner+Gain by board position and distance,
mono cues honest as center-panned point sources) on top of the SFX
generator's decoded-buffer cache and in-flight coalescing; GM-only
gate mirrored from sfx/ambience; failures surface verbatim.

Task B: LocalVadMeter folds Silero VAD burst durations into an EMA of
speaking seconds with half-life decay; local-mic dot strip in the
narrative telemetry row with unsupported/denied+Retry/idle/live
states; honest copy ($local mic, hears only this device$); never
touches getUserMedia itself. Local-only by design — server-side VAD
aggregation is a future iteration.

Gates: vitest 751 passed / 57 files (+35); relay 66; tsc+vite build clean in 5.6s.

Co-Authored-By: Claude <[email protected]>
2026-08-26 16:32:46 -04:00
John SmithandClaude 6b5dc6f95e docs: Loop3 tracker rows for iterations 28 and 29
Co-Authored-By: Claude <[email protected]>
2026-08-26 12:10:18 -04:00
John SmithandClaude e655ac370d fix(client): audit-sweep F3 admin role widening sweep (loop 3, iteration 29)
Lifts the role→ids decision out of App.tsx into a pure
src/api/seat_authority.ts helper (gm|admin → full visible list, player
→ filtered by assignedTokenIds+isPlayer, else []) so the F3 invariant
can be pinned by a unit test without mounting App. Applied the
gm||admin widening to the FUNCTIONAL sites — controlledEntityIds
(App.tsx), defaultOpen (NarrativeChat), canManageAtmosphere
(App.tsx), isGm → InitiativeTracker (App.tsx) — plus type/prop
widening on QuestJournalModal. POLICY GM-ONLY sites left intentionally
narrow per the docstring: canEnterStreamerView (streamer broadcast is
an authority decision about what leaves the room), Navbar Admin
Console, handleBeginCombat/handleEndCombat (gate on spectator, not
gm).

Gates: vitest 716 passed / 54 files (+6 tests / +1 file); relay 66;
tsc+vite build clean in 5.1s.

Co-Authored-By: Claude <[email protected]>
2026-08-26 12:10:06 -04:00
John SmithandClaude a52bcd4dfc fix(gateway): audit-sweep remediations F1/F2/F5 (loop 3, iteration 28)
F1 (HIGH): per-session chat message cap (CHAT_MESSAGE_CAP = 10_000) auto-
trims the head on overflow in both MemoryStore and PostgresStore; next
id computed before trim so trimmed ids never reappear. New DELETE
/api/v1/sessions/{id}/chat/{message_id} reuses the timeline gate
(404 SESSION_NOT_FOUND / 403 outsider) plus author-or-staff check
(403 CHAT_DELETE_FORBIDDEN; 404 CHAT_MESSAGE_NOT_FOUND).

F2 (MED): timeline projection now also collapses actor_id to None and
filters referenced_entity_ids to drop hidden ids when the hidden-entity
collapse branch fires — visible ids still survive. Regression test
asserts no hidden UUID appears anywhere in a non-GM projected entry.

F5 (LOW): system-channel write gate tightened to {gm, admin} only,
mirroring the gm-channel posture; runtime gate only, schema stays
forward-compatible.

Gates: pytest 1302 passed / 33 skipped (baseline 1288, +14).

Co-Authored-By: Claude <[email protected]>
2026-08-26 12:09:25 -04:00
John SmithandClaude 3d7de5910e docs: Loop3 tracker row for iteration 26
Co-Authored-By: Claude <[email protected]>
2026-08-26 11:38:36 -04:00
John SmithandClaude fd0f36ba5c feat(gateway): session event timeline merging narrative + engine ledger (loop 3, iteration 26)
GET /api/v1/sessions/{id}/timeline returns the chronological merge of
narrative chat and engine ledger events (engine sequence_ids namespaced
e_<n>, narrative n_<n>), sorted by (created_at_ms, kind, sequence_id),
paginated by an opaque cursor with default 100 / cap 500. Hidden-entity
projection lifted from the existing _entity_is_visible convention;
private channels (gm/whisper) collapse to "Something happens" for
players and drop entirely for spectators. ALSO had to add the missing
narrative chat log + POST /chat route — survey found NarrativeChat
consumed chat from React local state only with no server anchor.
Summary numbers (ATTACK_RESOLVED damage, HP, etc.) are stripped from
spectator view to avoid leaking tactical information.

Gates: pytest 1288 passed / 33 skipped (baseline 1248, +40).

Co-Authored-By: Claude <[email protected]>
2026-08-26 11:38:24 -04:00
John SmithandClaude c467301f12 docs: Loop3 tracker row for iteration 25
Co-Authored-By: Claude <[email protected]>
2026-08-26 11:35:45 -04:00
John SmithandClaude cf8745ed5e feat(client): condition-themed token rings + lucide badge stack (loop 3, iteration 25)
Each of 11 SRD conditions now drives a Tailwind ring color and a
lucide-react icon badge over the TacticalCanvas token; multiple
conditions render in 5e-severity priority order (unconscious > para >
stunned > restrained > blinded > frightened > charmed > grappled >
deafened > prone > poisoned) with a +N overflow past three. Engine-only
flags (incapacitated/invisible/petrified/exhaustion) are intentionally
dropped to keep the wire honest. Hidden-entity invariant double-applied:
the App projection still drops hidden ids, and a defensive
isVisible !== false guard wraps the new ring/badge surface so a leaked
hidden token never paints a condition color or badge.

Gates: vitest 710 passed / 53 files (+44); relay 66; tsc+vite build clean in 5.4s.

Co-Authored-By: Claude <[email protected]>
2026-08-26 11:35:07 -04:00
John SmithandClaude f27a38ba0d docs: Loop3 tracker rows for iterations 23 and 24
Co-Authored-By: Claude <[email protected]>
2026-08-26 11:23:29 -04:00
John SmithandClaude 0dab7eb483 fix(client): audit-sweep remediations F10/F11/F12 + EncounterBuilderView latch (loop 3, iteration 23)
F10: ambience_store now subscribes to sign-out/role change and clears
its decoded-AudioBuffer cache, the in-flight coalescing map, and the
loop player on logout — a GM-signed-in seat cannot replay a bed
after demotion to player.

F11: deleteCharacter returns a discriminated-result union mirroring
encounter_balance_store (ok | not_signed_in | forbidden | not_found |
unreachable | rejected); MyCharactersView renders the gateway's
verbatim detail rather than the previous fabricated disjunction.

F12: userRole plumbed through AudioMixerModal into both AmbiencePanel
and SfxPanel so non-staff seats render only the lock notice and never
fire listing/generation requests. SeatRole union extended with
'admin' to match the server's role taxonomy; sfx_panel + character
gallery tests updated for the new contract.

EncounterBuilderView: forbiddenByServer now re-evaluates on token/
userRole change instead of latching for the component's lifetime, so
a GM signing in after a player's 403 regains the balance strip.

Gates: vitest 666 passed / 51 files (+24 vs 642/48); relay 66 passed; tsc+vite build clean in 5.45s.

Co-Authored-By: Claude <[email protected]>
2026-08-26 11:23:18 -04:00
John SmithandClaude d95fd6787b feat(gateway): safety boundaries summary endpoint (loop 3, iteration 24)
GET /api/v1/sessions/{id}/safety/boundaries/summary returns aggregate
counts only ({you.{lines,veils}, others.{lines,veils}, redacted.lines})
so client UIs can render declaration-count badges without pulling the
full topic list. Reuses iter20's _boundary_gate for auth/HMAC/404/403.
The response body carries no topic strings, entry ids, actor ids, or
timestamps — enforced by a canary-string sweep test that fetches the
full listing first to harvest strings, then asserts the summary body
contains none of them. Empty registry returns all zeros (no 404).

Gates: pytest 1248 passed / 33 skipped (baseline 1232, +16).

Co-Authored-By: Claude <[email protected]>
2026-08-26 10:38:15 -04:00
John SmithandClaude da1133d4d0 docs: Loop3 tracker row for iteration 22
Co-Authored-By: Claude <[email protected]>
2026-08-26 10:01:40 -04:00
John SmithandClaude 1e7028801b fix(gateway): audit-sweep remediations F1/F7/F8 (loop 3, iteration 22)
F1: /api/v1/engine/delay and /api/v1/engine/delay/resume proxies added
mirroring the dodge/dash/disengage template (EngineEntityActionRequest +
_maneuver_proxy) — the client's existing Delay/Resume UI now actually
reaches the engine; engine verdicts surface verbatim.

F7: encounter-balance roster capped at min=1/max=64; new
_require_staff_role sub-dependency hoists role verification ahead of
body parsing so a non-staff caller with an oversized/malformed body
gets 403 without the gateway allocating/validating the array; route
moved into the tight media bucket (10/min) with rationale documented.

F8: ambience coalescing rewritten with asyncio.shield + an _adopt done-
callback — leader cancellation propagates only to the leader; the
upstream generation continues to populate the cache for later callers,
and late joiners keep coalescing onto the adopted task (exactly 1
upstream call). Normal failure paths still propagate via the shared
future so coalesced-failure semantics are preserved; cap violations
(413) also wake waiters.

Gates: pytest 1232 passed / 33 skipped (baseline 1217, +15).

Co-Authored-By: Claude <[email protected]>
2026-08-26 10:01:25 -04:00
John SmithandClaude 7f0c90ba92 docs: Loop3 tracker row for iteration 21
Co-Authored-By: Claude <[email protected]>
2026-08-26 09:59:34 -04:00
John SmithandClaude aacea1b5f7 fix(engine): audit-sweep remediations F2/F3/F4/F5/F6/F9 (loop 3, iteration 21)
F2 delay/delay-resume responses now carry only status/entity_id/event_sequence
(no unprojected order/delayed leak — clients re-pull the projected snapshot).
F3 apply-condition: GM-only on unowned entities (no more player→monster
fiat affliction). F4 advance_round now consumes the delayed list in
production (was only ever honored in tests); TURN_ADVANCED journals
turn_index + a condition_clocks snapshot. F5 begin_combat clears stale
delayed, and rewind scopes DELAY_TAKEN/DELAY_RESUMED replay to events
after the last surviving COMBAT_BEGAN. F6 CONDITION_APPLIED carries
end_of_turn_save so save config survives rewind; remaining_rounds
restored from the last surviving TURN_ADVANCED's condition_clocks
snapshot. F9 source-oracle collapse: unknown and hidden source ids
return byte-identical SOURCE_ENTITY_NOT_FOUND bodies.

Documented residuals: CONDITION_EXPIRED events are not yet consulted by
the timed-condition replay (separate rewind class beyond F6's scope);
round value is not replayed from TURN_ADVANCED (kept as-is to avoid
scope creep).

Gates: cargo test --workspace 500 passed / 21 suites (+10); clippy clean.

Co-Authored-By: Claude <[email protected]>
2026-08-26 09:59:23 -04:00
John SmithandClaude 8cc2f97967 docs: Loop3 tracker row for iteration 20
Co-Authored-By: Claude <[email protected]>
2026-08-26 09:15:39 -04:00
John SmithandClaude effcf0006f feat(gateway): Lines &amp; Veils per-session safety registry (loop 3, iteration 20)
GOALS.md pillar 11 safety toolkit beyond the X-Card: per-session boundary
registry under /api/v1/sessions/{id}/safety/{boundaries,lines,veils}.
Any participant may declare under their own actor id; GM/admin manage
all; deletes are author-or-staff. Load-bearing redaction rule: veils are
shared openly, but a non-staff viewer sees OTHER participants' line
topics as [redacted] (metadata intact) while their own stay verbatim.
Topics trimmed/capped at 120 chars with machine-readable 422 codes;
in-process storage matching the established session-scoped-state
convention — documented trade-off: restart drops declarations rather
than persisting the most privacy-sensitive strings.

Gates: pytest 1217 passed / 33 skipped (+29 vs same-environment control).

Co-Authored-By: Claude <[email protected]>
2026-08-26 09:15:09 -04:00
John SmithandClaude 681c99754e docs: Loop3 tracker row for iteration 19
Co-Authored-By: Claude <[email protected]>
2026-08-26 06:09:24 -04:00
John SmithandClaude f9675f0596 feat(engine): Frightened source attribution + movement enforcement (loop 3, iteration 19)
Survey confirmed condition round-tracking and concentration already
existed; the gap was Frightened/Charmed enforcement. ConditionTimer now
carries an optional source_entity_id (legacy-safe, journaled in
CONDITION_APPLIED), and move_entity refuses willing approach toward a
Frightened victim's live fear source (FRIGHTENED_CANNOT_APPROACH);
unattributed legacy fright enforces nothing. Source lookup resolves from
surviving ledger events so it survives restore and rewind — which also
fixes safety_rewind never un-granting timed conditions backed by reverted
CONDITION_APPLIED events. New POST /action/apply-condition route with
sibling-identical RBAC; projections leak nothing new.

Gates: cargo test --workspace 490 passed / 21 suites (baseline 477); clippy --all-targets clean.

Co-Authored-By: Claude <[email protected]>
2026-08-26 06:08:21 -04:00
John SmithandClaude 1026da6e0a docs: Loop3 tracker row for iteration 18
Co-Authored-By: Claude <[email protected]>
2026-08-26 05:36:03 -04:00
John SmithandClaude e2a7f9892d feat(client): ambience preset panel in the audio mixer (loop 3, iteration 18)
GM-gated panel over iteration 17's /media/ambience routes: six curated
looping soundscapes with server-cached badges, one bed at a time via
BufferSource(loop) -> GainNode. Decoded-AudioBuffer cache keyed by slug,
per-slug in-flight coalescing, failures never cached; non-staff seats get
the same lock-notice convention as the SFX panel and the gateway's 403
copy surfaces verbatim when roles aren't plumbed.

Gates: vitest 642 passed / 48 files (+26); tsc+vite clean in 4.8s; relay suite 66.

Co-Authored-By: Claude <[email protected]>
2026-08-26 05:35:46 -04:00
John SmithandClaude fa20c39478 docs: Loop3 tracker row for iteration 16
Co-Authored-By: Claude <[email protected]>
2026-08-26 05:11:17 -04:00
John SmithandClaude 7acddadebc feat(client): Delay/Resume controls in the initiative tracker (loop 3, iteration 16)
Surfaces iteration 15's engine Delay action: sky-toned hourglass badge for
every id in combat.delayed (trusted verbatim from the role-projected
snapshot, never re-filtered client-side), Delay/Resume buttons gated to
entities this seat controls mirroring the engine's may_control_entity
RBAC, engine rejections (ALREADY_DELAYED, NOT_DELAYED, ...) surfaced
verbatim in an alert strip. Success re-pulls the combat snapshot — no
local mutation of order/turn_index.

Gates: vitest 616 passed / 46 files (+28); tsc+vite clean in 4.7s; relay suite 66 passed.

Co-Authored-By: Claude <[email protected]>
2026-08-26 05:10:58 -04:00
John SmithandClaude add3d7f538 docs: Loop3 tracker row for iteration 17
Co-Authored-By: Claude <[email protected]>
2026-08-26 04:56:50 -04:00
John SmithandClaude 6f1c03aca1 feat(gateway): curated ambience presets with bounded LRU cache (loop 3, iteration 17)
Six D&D soundscape presets (tavern murmur, dungeon drips, forest night,
battle clash, thunderstorm, campfire) generated on demand via the SFX
capability. GET /media/ambience lists presets to any authenticated seat;
POST /media/ambience/{slug} is GM/admin-only like /media/sfx. In-process
LRU (12 entries, keyed by slug+model) absorbs repeat spends; concurrent
duplicates coalesce onto one upstream call; failures and over-cap
payloads are never cached and wake all waiters.

Gates: pytest 1190 passed / 31 skipped (baseline 1151, +39).

Co-Authored-By: Claude <[email protected]>
2026-08-26 04:56:18 -04:00
John SmithandClaude 5eb45237a6 docs: Loop3 tracker row for iteration 15
Co-Authored-By: Claude <[email protected]>
2026-08-26 04:39:32 -04:00
John SmithandClaude 4f7b365b84 feat(engine): SRD-optional Delay action, rewind-consistent (loop 3, iteration 15)
POST /sessions/{id}/action/delay parks a combatant out of turn order
(free — the cost is forfeiting this round's slot); /action/delay/resume
re-seats them at the current initiative count. next_turn skips delayed
slots with a bounded scan (all-delayed terminates parked). Flags replay
from DELAY_TAKEN/DELAY_RESUMED ledger events under safety_rewind, clear
on end_combat, and prune for despawned entities. RBAC matches attacks
(own entity only; spectators 403); non-GM snapshot projection now drops
hidden ids from combat.delayed as well as combat.order.

Gates: cargo test --workspace 477 passed / 21 suites (baseline 466);
clippy --all-targets clean.

Co-Authored-By: Claude <[email protected]>
2026-08-26 04:39:08 -04:00
John SmithandClaude 762a5e26c5 docs: backfill Loop3 iteration tracker rows 7-14
Co-Authored-By: Claude <[email protected]>
2026-08-26 04:25:25 -04:00
John SmithandClaude d1ff6c58be feat(client): GM-visibility hardening for balance strip (loop 3, iteration 13)
The debounced server-balance strip now retracts itself permanently for any
seat the gateway answers 403, and treats a missing session token as
not-signed-in (strip hidden) instead of waving an error banner. Explicit
isGM prop still wins when App.tsx wires it; unknown-monster detail
parsing moved entirely into the store module.

Gates: vitest 588 passed / 44 files; tsc+vite build clean in 4.7s.

Co-Authored-By: Claude <[email protected]>
2026-08-26 04:24:39 -04:00