Shelf: reveal to table — player-safe layer, multi-surface (#245) #265
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/v3.11-reveal-to-table"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #245.
A shelf card is GM-only. Revealing it shows the card's player-safe layer on one or more surfaces and records what was shown.
Scoped per the discussion on approach: Discord and a "show to players" surface for logged-in members (players already have Quest Board logins, so this needs no public/unauthenticated route), with the dispatch built as a registry so the Foundry pillar can plug in later.
The player-safe layer
New
shelf_cards.player_text— what the table may see, as against the rest of the card, which stays GM-only.When the GM hasn't authored one, the server derives a conservative default:
summary, nevergm_notesread_aloud,one_liner,paragraph,flavor)That derivation is a strict allowlist, not a denylist: an unknown tool's output contributes nothing rather than leaking whatever it happens to call its secret. When it comes out empty the reveal is refused (422) rather than guessed at — the safe failure mode for a redaction feature.
Whatever does get shown is saved back onto the card, so the GM sees exactly what the table saw and can edit it, instead of revealing blind on every showing.
Why reveals are rows, not a flag
shelf_revealsexists as its own table because:shelf_card_idisON DELETE SET NULL, so discarding a card in the end-of-session sweep (#247) doesn't erase the record of what the table was shown.revoked_attakes a reveal back down without deleting the history of it having been shown.Surfaces are a registry
Adding a surface — Foundry "push into the live scene" for v4.x — is one entry here plus one value in
REVEAL_TARGETS. No schema change, no migration, no new column.Dispatch runs before the row is persisted and is all-or-nothing, so a surface that refuses (Discord with no channel configured) never leaves behind a reveal claiming the table saw something it didn't.
Authorization
Reading reveals is open to every campaign member — it is the players' surface, via
get_session_for_member. Revealing and revoking stay GM-only. The board itself remains GM-only and is unchanged.Cross-component
shelf_revealis an additive event on the durable bot-event stream, soBOT_CONTRACT_VERSIONstays 1 (it governs the/api/bot/*shape, not the event vocabulary). Backend and bot move in this one commit per the monorepo rule. The bot postsplayer_textverbatim, never enriches it from another source, and never logs the revealed text.Frontend
/sessions/:id/table— the players' mirror of the shelf, linked from SessionDetail for every member. Polls rather than streams; a failed poll leaves the last good list on screen instead of replacing the table's reference with an error mid-scene.Migration
d9e0f1a2b3c4(head, fromc8d9e0f1a2b3). Applied cleanly against a full chain replay from empty.Testing
All run locally in containers matching the CI jobs:
test_shelf_reveal.pycovering the explicit/derived/refused text paths,gm_notesandsecretnever leaking, the target registry (including the unknown-target and failed-dispatch-reveals-nothing cases), the member-read/GM-write boundary, revoke idempotency, frozen text, and the reveal surviving its card.player_textno-op.ruff check/ruff format --checkclean;eslintclean (the one warning is pre-existing inCampaignDetail.jsx).🤖 Generated with Claude Code