The Rail · Reference & analysis

V8 Case Files → V10 Rail channel integration

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:

8sections15 minread1table

On this page

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 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 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 Player model/history or explicit season parameters Produces or queues CaseSpecs 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 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 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 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 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 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 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 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 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.

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:

  • 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: 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 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:

  • 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. 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 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 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 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 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.