# V8 Case Files Rail channel

`@oshun/v10-rail-channel-case-files` adapts the leak-safe V8 evidence-board
snapshot to the V10 ambient Rail contract. The first implemented surface is a
motionless, two-second-legible presence tile:

- case identity;
- suspect and released/total clue counts;
- player-only pin and contradiction counts;
- time remaining until (or inside) the accusation window; and
- one state-derived line for text-only surfaces.

The adapter accepts a `CaseFilesBoardStateReader`; production composition must
inject the durable per-player projection store.
`InMemoryCaseFilesBoardStateStore` exists for tests and local single-process
composition only. Every read is parsed through `CaseFilesBoardStateV1Schema`,
whose strict shape excludes cooked truth, unreleased evidence, and pre-reveal
fairness receipts.

Cold-cache ownership remains with `RailChannelHost`: after a successful render,
export its tile-cache record and restore it on the next host before a live board
reader is available. The state-derived `textOnlySummary` is retained in that
same validated cache record.

## Exact board-moment handoff

Every rendered tile carries a validated, state-derived deep link in addition to
the manifest's static fallback. Its moment context binds the case and edition to
the tile face, board revision, board update instant, observation instant, phase,
and `board-snapshot` view. A cold-cache restore therefore opens the exact
snapshot that produced the glance instead of silently advancing to the newest
board. The standard presence surface exposes this as one restrained “Open
moment” action only when a dispatcher is connected; text-only discretion mode
suppresses the handoff and its case-specific destination copy.

Scheduled case-open, clue, witness, and reveal drips likewise target the case
board with their exact source event id, release kind, occurrence instant, and
clue/witness reference where applicable. Product composition must pass these
links through `RailChannelHost.resolveDeepLink` or `dispatchDeepLink`; the
adapter does not construct an external URL or bypass the Rail's registered
owning-product boundary.

`CaseFilesRailChannel.dripsBetween` maps the cooked schedule's case-open, clue,
and witness releases only after they become due in an exclusive/inclusive poll
window. It whitelists the public payload for each kind, verifies clue/witness
references, and produces deterministic Rail event ids. Callers pass those events
to `MergedTimeline.emitterFor(v8.case-files)`; the channel has no flush,
priority, or delivery controls. With default dayparts, the 08:00 local case-open
release is part of the morning batch, while deep-work evidence waits for the
next break under R0.4.

## Accusation window and reveal

The manifest declares accusation opening as a rare scheduled live moment with
wind-down affinity and at least 60 minutes of notice. `accusationLiveMoment`
derives its stable request and announcement ids from the cooked schedule and
uses that schedule's product-zone opening instant. The Rail arbitrator remains
the authority for pre-announcement, effective loudness, daypart eligibility,
ring policy, and elevation budgets.

The reveal is different from an ordinary public drip. Only when the reveal
instant enters the poll window does the adapter read its injected sealed
`CaseFilesFairnessReceiptSource`. It then binds the strict signed receipt to the
case, edition, accusation instant, reveal instant, source event, and product
zone before emitting `case.revealed`. A missing or mismatched sealed artifact
fails closed, and the source is never read during pre-reveal polls.

## Faces and post-reveal social reads

The player face remains the injected player-scoped board: pins and contradiction
counts never enter the spectator payload. Before reveal, the spectator face is
only today's public case state and the adapter does not call social storage.
After the board itself advances to `revealed`, the spectator face requires a
strict `CaseFilesCommunitySolveStatsV1` aggregate bound to the same case,
edition, and reveal instant. Its submitted/correct counts must reproduce the
reported basis-point rate exactly; future or mismatched aggregates fail closed.

`compareDeductions` is the server-side social gate. Viewer identity comes from a
separate authenticated server context and is forbidden in the serialized client
request. The gate rejects before reveal without calling
`CaseFilesSocialSource.readSharedDeductionBoards`. After reveal, it returns only
the strict minimal friend projection (public participant label, pin ids, visible
edge ids, contradiction node ids, and optional accusation), never a raw player
board. Every returned id must exist on the revealed public case, the viewer
cannot be returned as a friend, and extra/private fields are schema-invalid.
Production composition must keep `CaseFilesSocialSource` behind this adapter
rather than exposing the underlying store to clients.

## Inline board actions

The manifest publishes three bounded acts: `pin-suspect`, `mark-contradiction`,
and `proof-tree-hint`. Pin and contradiction inputs carry the desired boolean
state rather than a toggle, so a retry is naturally safe. The injected
`CaseFilesMicroActStateStore` must atomically commit a one-revision board
update, the per-player hint cursor, and an execution-id receipt. The local
in-memory implementation also rejects attempts to reuse one board overlay for
another player.

Production hint composition uses the package's explicit
`./case-engine-proof-tree-source` export. It binds sealed case authority to
Clew's `minimalProofTreeHint`: one new clue is selected from the verified
deduction path only when that clue is already present on the released board. The
completion payload contains that clue's public name and description, never the
solution conclusion, culprit, or an unreleased clue. The source must remain
server-side; clients receive only the validated minimal result.
