# V8 Case Files → V10 Rail channel integration

**Audit date:** 2026-07-17

**Scope:** RA.5 source-first integration note for the communal daily-case cook,
evidence-board projection, and post-reveal fairness receipt.

## Integration decision

The daily Case Files cook should use the existing **Loom release pipeline** as
its authoritative generation path, then add a Rail-owned wall-clock release
schedule, board projection, and signed fairness receipt:

```text
communal daily selector / director
  → CaseSpec + CanonContext
  → Loom: Clew → Minos → narrative/assets → Daedalus draft
          → Theseus + safety + canon gates → Daedalus publish compile
  → Case Files cook: release schedule + public board seed + sealed proof
  → Ed25519 fairness receipt, served only after reveal
```

There are two important representation boundaries:

1. [`runPipeline`](../apps/v8/loom-service/src/pipeline.ts) is the most complete
   existing publish path. It consumes the engine's `CaseSpec`/`CanonContext`,
   uses Minos's ASP-or-DPLL proof, runs the canonical seven pipeline gates, and
   returns the draft `CompiledCase` when blocked. Only a green release decision
   replaces it with the final publish compile after the Python schema gate.
2. [`forgeCaseBundle`](../libs/v8/case-bundle/src/case-bundle.ts) is a separate,
   narrower product forge. It consumes a V8 `MysterySkeleton`, runs the finite-
   domain CSP prover, gates a `V8Case`, and Ed25519-signs **asset manifests**.
   It neither consumes nor emits the engine/compiler `CompiledCase` model.

No public adapter currently converts `CaseGroundTruth` into `MysterySkeleton`,
and no case-level fairness-receipt type or signing function exists. The daily
cook must close those seams explicitly; it must not present the asset C2PA
signature or the verifier's unkeyed report hash as the fairness receipt.

## Audited public API map

| Layer               | Public entry points                                                                                                             | Inputs                                                                                           | Output / role in the daily cook                                                                                                                                                                                                        |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Director            | [`nextCaseSpec`, `pacedDifficulty`, `MintQueue`, `placeCase`, `buildSeasonCalendar`](../libs/yemaya/case-director/src/index.ts) | Player model/history or explicit season parameters                                               | Produces or queues `CaseSpec`s and selects placement. `nextCaseSpec` is player-specific; `buildSeasonCalendar` is weekly. Neither API creates a communal **daily** selector or wall-clock clue schedule.                               |
| Engine (Clew)       | [`generateCase`, `generateCaseSync`](../libs/yemaya/case-engine/src/clew.ts)                                                    | `CaseSpec`, `CanonContext`, optional `CreativeProposer`                                          | Solve-first `MysterySession`, hidden `CaseGroundTruth`, and red-herring clue ids. The sync path is deterministic; the optional LLM may enrich surface flavor only.                                                                     |
| Verifier (Minos)    | [`verifyCase`](../libs/yemaya/case-verifier/src/minos.ts)                                                                       | `MysterySession`, `CaseGroundTruth`, optional available-clue override                            | G1 uniqueness through clingo or in-process DPLL, G2 deduction completeness, G3 fair play/balance, difficulty, and a `VerifierReport`.                                                                                                  |
| Compiler (Daedalus) | [`compileCase`](../libs/yemaya/case-compiler/src/compile.ts)                                                                    | Verified session + ground truth + compile options, optional assets                               | `CompiledCase`: V5 pack, evidence nodes, deduction edges, accusation outcomes, generated manifest, and resolved asset bundle. Publish compile requires every supplied release gate green and the Python schema gate.                   |
| Eval/release        | [`checkInGameSolvable`, `judgeCase`, `decideRelease`](../libs/yemaya/case-eval/src/index.ts)                                    | Draft `CompiledCase`, Minos results, independent safety/canon results                            | G4 compiled-game solve, G5 quality verdict, and the canonical all-seven release decision.                                                                                                                                              |
| V8 CSP              | [`compileMysteryToCsp`, `proveCaseUniqueness`](../libs/v8/case-csp/src/mystery-skeleton.ts)                                     | `MysterySkeleton` containing dimensions, intended assignment, and presented/withheld constraints | Independent finite-domain proof over player-visible clues. A unique solution counts only when it matches the intended ground truth.                                                                                                    |
| V8 gates            | [`evaluateV8Case`](../libs/v8/case-gates/src/index.ts)                                                                          | `V8Case` plus optional thresholds                                                                | Shared release-gate service over fairness, unique solvability, clue grounding, voice, bounded misdirection, prose, and safety. These gate ids have different numbering from the canonical Loom pipeline; map by name, never by number. |
| V8 bundle           | [`forgeCaseBundle`, `verifyCaseAssetManifest`](../libs/v8/case-bundle/src/case-bundle.ts)                                       | `MysterySkeleton`, prose writer, independent scorer, optional media providers and signer         | CSP-proven/gated prose-and-media bundle plus per-asset Ed25519 manifests. Unsupported providers remain `not-configured`; absent writer fails loud.                                                                                     |
| Shared signing      | [`Ed25519ClaimSigner`, `Ed25519ClaimVerifier`, byte/base64 helpers](../libs/shared/content-signing/src/index.ts)                | PEM key material, key id, canonical bytes                                                        | The one portfolio Ed25519 implementation. This is the cryptographic seam the fairness receipt must reuse.                                                                                                                              |

The checklist lists the packages in audit order
(`compiler → director → engine → gates → bundle`), but executable data flows in
the order above: the director or cook first creates the spec, the engine creates
truth, verification disposes, and the compiler runs after verification.

## Inputs for one communal daily run

### 1. Deterministic cook identity

Each run must fix these rather than reading ambient defaults inside the pure
cook. The scheduler supplies the date, zone, issuance instant, and signing
configuration; the implemented communal key is the fixed `v10.case-files` seed
namespace:

- a calendar date and stable communal audience key;
- a deterministic seed derived from that identity;
- a case id that includes the date or other immutable edition id;
- a `weekday` or `weekend` variant derived deterministically from that calendar
  date;
- the release-zone policy, from which the cook derives and persists explicit
  instants for opening, clue window, accusation opening/closing, and reveal;
- a mandatory fixed `issuedAt` instant for generated records; and
- governed model-version identifiers used by
  [`reproKeyOf`](../libs/yemaya/case-pipeline/src/determinism.ts).

The worldwide case cannot call `nextCaseSpec` with an individual player's id:
that would produce different cases. The cook should construct one communal
`CaseSpec` from the date/edition policy, reusing the director's deterministic
seed utilities and difficulty bands. The current director has no daily API, so
that selection adapter belongs to the cook task.

### 2. Structural case inputs

The engine's exact required input is a validated
[`CaseSpec`](../libs/yemaya/case-contracts/src/case-spec.ts):

- `caseId`, integer `seed`, `cell`, `era`, and `difficulty`;
- difficulty-band-valid `targetSuspects` and `targetClues`;
- `themeTags`;
- optional source-cell and cast constraints;
- target voice-over length; and
- optional commissioner identity.

It also requires a bounded
[`CanonContext`](../libs/yemaya/case-contracts/src/canon-context.ts): snapshot
hash, cell/era, relevant entities, prior-case summaries, hard constraints, and
open threads. The snapshot hash is part of reproducibility and must be retained
with the cooked edition.

An optional `CreativeProposer` may enrich names/title/prose. With no proposer,
the deterministic tables produce a complete case. A configured provider error
propagates; malformed enrichment is rejected rather than treated as successful
generation.

### 3. Release-pipeline dependencies

The existing Loom orchestration additionally needs:

- a per-case `CaseBudget` and `StageJournal`;
- a required `SafetyScanner`;
- a canon-consistency checker for G7 (absence blocks publish);
- optional narrative completion, independent judge, and asset-realization
  providers;
- model versions, repository root for the Python manifest gate, and fixed record
  time.

The pipeline may produce a text-only degraded pack when no asset realizer is
wired, but the release gates still must pass. The daily product policy can be
stricter if its featured edition requires portraits, VO, or other modalities.

### 4. Rail cook inputs not represented in V8

V8 clue timing is story-relative: `Clue.introducedAt.percentage` and
`solutionRevealPoint` are percentages, not UTC release times. The Case Files
cook must add:

- a monotone wall-clock `ReleaseSchedule` mapping every public clue/witness to
  an instant;
- morning-open and evening accusation/reveal events;
- a guarantee that every receipt/solution clue was released before the
  accusation deadline;
- a stable configured Ed25519 `ClaimSigner`; and
- storage for the immutable cooked bundle, public board seed, sealed solution,
  and signed receipt.

The cook now persists the schedule in canonical cooked JSON. Recomputing release
instants from a user's current timezone would change the historical proof.

### 5. Persisted provenance

The cooked edition contains `CaseFilesCookProvenanceV1` beside the complete
signed receipt. It retains the case id, edition date/profile, seed, canon hash,
repro key, cook/model versions, signer key id, issuance instant, and SHA-256
bindings for the verifier report, compiled case, release schedule, and receipt
payload. The CLI requires the issuance instant explicitly, writes mode `0600`
canonical JSON through an atomic no-clobber create, accepts a byte-identical
re-cook as `unchanged`, and refuses content drift or group/world-readable
output.

### 6. Tomorrow scheduler and deployment seam

The one-shot Nx `cook-tomorrow` target resolves tomorrow and today's stable
local-midnight batch instant in `CASE_FILES_TIME_ZONE`, requires explicit key
paths/key id/output storage, and invokes the same cook CLI. It is safe for an
external cron runner to retry because identical bytes report `unchanged`.
[`OPERATIONS.md`](../apps/v10/case-files-cook/OPERATIONS.md) records the locked
single-host cron command and the honest production seam: platform scheduling and
leader/concurrency control, secret-manager key mounts, durable encrypted
storage/promotion, non-zero-exit and missing-edition alerts, and governed key
rotation.

## Representation and proof boundaries

### Engine and compiled case

Clew's `MysterySession` is the richest source for daily timing and proof:

- `characters` supplies victim/suspect public identities;
- `clues` supplies descriptions, visibility, story percentage, and stable ids;
- the correct `Solution` supplies required/supporting clues and the ordered
  deduction path; and
- `timeline.solutionRevealPoint` defines the latest fair reveal boundary.

Daedalus turns this into the shipping
[`CompiledCase`](../libs/yemaya/case-contracts/src/compiled-case.ts):

- `evidenceNodes` are the display substrate;
- `deductionEdges` are board connections;
- `accusationOutcomes` contain correct and plausible-wrong result branches;
- `manifest` binds the verifier report id, seed, gate results, assets, and repro
  key.

Some compiled fields are answer keys. In particular, `bFalseLead`, the correct
accusation outcome, solution evidence chain, and required edge ids must remain
sealed until reveal.

### Two uniqueness provers

The production engine path uses Minos. It compiles the
`CaseGroundTruth.constraints` reachable from non-withheld clues to CNF and asks
clingo or DPLL which suspects remain viable. A pass requires exactly one viable
suspect and a match to ground truth.

The V8 CSP package instead needs a `MysterySkeleton` with arbitrary finite
dimensions and clue constraints. It enumerates up to two solutions and reports
unique, under-determined, or unsatisfiable.

Both are real provers, but there is no translation between their models. If the
daily cook is required to record both CSP and DPLL evidence, it must introduce a
tested, semantics-preserving engine-to-skeleton adapter. It must not reconstruct
a skeleton from prose or set `uniqueSolutionProven` from the Minos boolean
without actually running the CSP.

### Pipeline result retention gap

`runPipeline` currently returns the compiled case and only a shortened verifier
report id. It does not return the original session, ground truth, complete
verifier report, or ordered deduction path. The receipt cook therefore needs one
of these honest changes:

- extend the orchestration result with a deliberately scoped proof artifact; or
- compose the same public stages in a cook orchestrator and retain their real
  results.

Re-running generation just to recover proof data is avoidable and would make
provider-enriched runs harder to audit.

## Evidence-board state

No audited V8 package exports an `EvidenceBoardState` or equivalent product
state. `MysterySession` is generation IR and `CompiledCase` is shipping content;
neither includes per-player pins, contradiction marks, accusation state, or a
wall-clock drip schedule.

The channel adapter should persist an immutable cooked edition separately from
the user's mutable board overlay, then project them into a cold-cache-safe
snapshot. This is the concrete target shape for that projection:

```ts
interface CaseFilesBoardStateV1 {
  schemaVersion: 1;
  revision: number;
  case: {
    caseId: string;
    editionDate: string;
    title: string;
    victim: { id: string; name: string };
    setting: string;
    phase: 'scheduled' | 'open' | 'investigating' | 'accusation' | 'revealed';
  };
  suspects: Array<{
    id: string;
    name: string;
    pinned: boolean;
  }>;
  evidence: {
    released: number;
    total: number;
    nodes: Array<{
      nodeId: string;
      sourceId: string;
      description: string;
      image?: string;
      releasedAt: string;
    }>;
  };
  deductions: {
    visibleEdges: Array<{
      edgeId: string;
      fromNodeId: string;
      toNodeId: string;
      text: string;
    }>;
    markedContradictionNodeIds: string[];
  };
  accusation?: {
    accusedId: string;
    submittedAt: string;
  };
  timing: {
    timeZone: string;
    opensAt: string;
    accusationOpensAt: string;
    accusationClosesAt: string;
    revealAt: string;
    nextReleaseAt?: string;
  };
  fairnessReceipt?: SignedFairnessReceiptV1;
  updatedAt: string;
}
```

Projection rules are part of the safety boundary:

- emit only evidence whose scheduled `releasedAt` is at or before the snapshot
  time;
- emit a deduction edge only after both endpoint nodes are released;
- keep `bFalseLead`, `pointsTo`, `eliminates`, ground-truth fact ids, hidden
  constraints, solution clue ids, and correct outcomes out of pre-reveal
  payloads;
- keep pins, contradiction marks, and submitted accusation in the per-user
  overlay, not in the communal cooked content;
- expose the receipt only in `revealed` phase; and
- derive the tile's suspect count, `released/total`, contradiction count, and
  accusation countdown from this snapshot so cold-cache and live render agree.

### Exact board-moment handoff

The implemented tile projection derives its deep link from the same parsed
snapshot as the glance. The context includes case id, edition, face, revision,
`updatedAt`, tile observation instant, and phase; the target route is the owning
V8 case board. Rail cache export/restore retains this context, so a later click
still identifies the exact snapshot that prompted it rather than meaning
“latest.” Static manifest links remain capability fallbacks only and are not
used by the connected presence action.

Wall-clock case-open, clue, witness, and reveal drips carry their own exact
release-event contexts (source event id, kind, occurrence instant, and optional
reference id). Both tile and timeline handoffs are resolved by the single
`RailChannelHost` target registry, which appends canonical `railMomentId` and
stable `railContext` query values for web and native-app destinations.

### Micro-act write and hint seam

The Rail adapter now treats player board actions as atomic overlay transactions.
Pinning and contradiction marking require an explicit target state
(`pinned: boolean` / `marked: boolean`), a current case id, and a released
target. A changed board advances exactly one revision; a repeated desired state
is a successful no-op. Each execution id is retained with its request
fingerprint and result, so transport retries return the original receipt while
an attempt to reuse the id for different input fails closed. Store instances are
player-scoped and reject cross-player reuse.

Proof-tree hints use Clew's `minimalProofTreeHint` through the adapter's
`CaseEngineProofTreeSource`. The engine walks the verified correct solution's
deduction path and selects exactly one clue id that is both released and not
previously taken. It returns the clue's existing name and description; it does
not synthesize a canned prelude or serialize the step conclusion. The player's
hint cursor and execution receipt commit in the same transaction, while sealed
solution authority stays on the server side.

The authoritative cooked edition may retain all answer-key material in sealed
storage. “Not serialized to the client” is the required barrier; merely hiding
fields in the UI is insufficient.

### Social faces and comparison boundary

The audited V8/Yemaya case packages contain no community solve aggregator or
friend-board read API. The Rail adapter therefore defines one narrow
server-owned `CaseFilesSocialSource` boundary. The player face continues to use
its player-scoped board reader. The spectator face omits all player overlay
fields and cannot read community storage before reveal; once the board has
advanced to `revealed`, it requires strict submitted/correct aggregate counts
whose exact basis-point rate, case, edition, reveal instant, and update time all
validate.

Friend comparison is a separate `compareDeductions` server operation. Viewer
identity is supplied independently by authenticated server context and is
schema-forbidden in the serialized client request. Before reveal the operation
rejects without invoking the source. After reveal it accepts only a minimal
shared projection of public suspect/evidence/edge ids, rejects the viewer and
duplicate participants, and cross-checks every id against the revealed public
board. Raw board objects, private notes, unreleased references, future
snapshots, and cross-edition results never cross the boundary.

## Ed25519 fairness receipt

### What exists

The canonical cryptographic implementation is
[`@oshun/content-signing`](../libs/shared/content-signing/src/index.ts). It
provides real Node `crypto` Ed25519 signing/verification, SHA-256, a byte-based
`ClaimSigner`/`ClaimVerifier`, and key-id-based verification.

The V8 bundle uses that implementation to sign a canonical payload for each
generated asset. Minos's `VerifierReport.checksumSha256`, by contrast, is a
deterministic SHA-256 content hash, not an Ed25519 signature. Neither artifact
is a case-level fairness receipt.

The bundle forge's fallback creates a fresh development key per forge. That is
useful in tests but unsuitable for daily production receipts: a daily cook must
inject a stable, securely managed private key and publish its `keyId` through a
trusted public-key registry. A client must not trust a public key solely because
the untrusted receipt embeds it.

### Receipt payload and envelope

The implemented cook uses a versioned schema with canonical serialization. The
signed payload binds:

```ts
interface FairnessReceiptPayloadV1 {
  schemaVersion: 'v10.case-files.fairness-receipt.v1';
  caseId: string;
  editionDate: string;
  seed: number;
  canonHash: string;
  reproKey: string;
  compiledCaseSha256: string;
  releaseScheduleSha256: string;
  proof: {
    verifier: {
      version: string;
      reportSha256: string;
      uniquenessBackend: 'clingo' | 'dpll';
      status: 'unique';
      matchesGroundTruth: true;
    };
    dpll: {
      status: 'unique';
      culpritId: string;
      viableCulpritIds: [string];
      matchesGroundTruth: true;
    };
    finiteDomainCsp: {
      status: 'unique';
      culpritId: string;
      solutionSha256: string;
      matchesGroundTruth: true;
    };
    uniqueCulpritId: string;
    solutionClueIds: string[];
    deductionPath: Array<{
      order: number;
      clueIds: string[];
      conclusion: string;
    }>;
    clueAvailability: Array<{
      clueId: string;
      releasedAt: string;
      availableBeforeAccusation: true;
    }>;
    releaseGates: {
      G1: true;
      G2: true;
      G3: true;
      G4: true;
      G5: true;
      G6: true;
      G7: true;
    };
  };
  accusationOpenedAt: string;
  revealedAt: string;
  issuedAt: string;
}

interface SignedFairnessReceiptV1 {
  payload: FairnessReceiptPayloadV1;
  algorithm: 'ed25519';
  keyId: string;
  signatureBase64: string;
}
```

The signing sequence is:

1. validate the payload schema and all timing/proof invariants;
2. serialize with a documented recursive key sort and UTF-8 encoding;
3. hash the compiled case and schedule over their own documented canonical
   forms;
4. call `ClaimSigner.sign(canonicalPayloadBytes)`;
5. store the returned base64 signature with `algorithm` and `keyId`; and
6. verify by reconstructing the same bytes and calling the trusted
   `ClaimVerifier`.

The receipt is generated after all release gates pass, stored with the cooked
bundle before the day begins, and withheld from clients until reveal. After
reveal it explains both **why** the answer is unique and **when** every
load-bearing clue became available.

### Browser verification seam

[`fairnessReceiptPayloadBytes`](../libs/contracts/src/v10/case-files.ts) is the
runtime-neutral canonical byte authority shared by the Node cook and browser.
The V10 client validates the strict receipt, selects an SPKI PEM solely from its
caller-supplied trusted registry by `keyId`, and checks Ed25519 with Web Crypto.
It never imports the Node signing entry point and never accepts a trust root
from the receipt. The local action has explicit valid, invalid-signature,
invalid-envelope, untrusted-key, and unavailable-runtime results.

### Rail delivery and live window

The channel manifest declares accusation opening as a `scheduled`, `rare` live
moment with wind-down affinity and a 60-minute minimum announcement. The adapter
derives stable request/announcement ids and the exact opening instant from the
cooked product-zone schedule; the kernel still owns pre-announcement, loudness,
daypart/ring policy, and budgets. At reveal—not before—the adapter reads its
sealed receipt source and emits a strict `case.revealed` payload only after the
receipt agrees with the schedule's case, edition, accusation, and reveal
instants. The Rail workspace then exposes the browser-local verification ledger.

## Fail-closed integration rules

- A communal edition is publishable only when the Loom release decision is green
  and the Daedalus Python build gate passes.
- If the later cook requires both V8 CSP and Minos DPLL evidence, either proof
  failing or disagreeing blocks the edition.
- Missing safety, canon, signing-key, or trusted-key configuration cannot be
  represented as success. If the V8 bundle forge is part of the chosen path, its
  missing independent scorer also blocks publish; Loom's optional judge seam
  otherwise reports its deterministic fallback honestly.
- Every scheduled solution clue must precede the accusation opening; the receipt
  validator rechecks this against persisted instants.
- Schedule, proof, compiled content, and receipt hashes are immutable for an
  edition. A repair creates a new edition/revision and a new signature.
- Answer-key fields and the receipt never cross the pre-reveal API boundary.

## Concrete seams and implementation status

The first daily-cook checklist leaf now implements the core seams without
inventing hidden APIs:

1. [`@oshun/v10-case-files-cook`](../apps/v10/case-files-cook/README.md)
   supplies the communal selector: Monday–Friday editions use the standard
   profile, while Saturday/Sunday editions use a larger hard-band case and a
   release day extended through 22:15 in the configured IANA zone.
2. Its `caseToCulpritSkeleton` adapter runs the required independent
   finite-domain CSP proof alongside the direct DPLL proof.
3. The cook retains the real verifier report and both proof results, maps story
   percentages to an immutable IANA-zone schedule, and compiles only after all
   seven release gates pass.
4. [`case-files.ts`](../libs/contracts/src/v10/case-files.ts) owns the strict
   serialized schedule, receipt, and leak-safe board snapshot schemas. The
   snapshot rejects future evidence, dangling deduction edges, pre-reveal
   receipts, invalid accusation timing, and non-IANA product zones.
5. The job signs through an injected `@oshun/content-signing` signer,
   self-verifies against the injected trusted verifier, and has no fallback
   signing key.
6. The one-shot tomorrow scheduler and its local/production operations seam are
   wired.
7. [`@oshun/v10-rail-channel-case-files`](../libs/v10/rail-channel-case-files/README.md)
   maps the cooked opening to the scheduled live-moment request and withholds
   its sealed receipt read until reveal.
8. The V10 browser renders that receipt as a restrained proof ledger and checks
   its real Ed25519 signature locally against a separately governed trust
   registry.
9. The two Rail faces now preserve the player/public split, add exact community
   solve aggregates to the spectator only after reveal, and gate minimal friend
   deduction comparisons on the server without even reading the social source
   pre-reveal.
