The skeleton remains embedded in the shipped bundle so presentation cannot erase the proof object that constrained it. A gate report is an authorization boundary: only a fully passing report permits the narrative and its provenance to become a signed delivery bundle.
V8's whole thesis — the LLM proposes; a constraint solver disposes — only holds if there is a precise, typed boundary between the two layers, and that boundary is a set of canonical data contracts: the typed shapes for a case — its solvable skeleton, its clues, its suspects and alibis, its proven solution, and the gated, signed bundle that ships — that every stage of the pipeline agrees on. Because the skeleton is machine-checked and the surface is regenerated, the contract is what lets a constraint solver, a release-gate suite, an LLM prose writer, and a C2PA signer all touch the same case without any one of them being able to redefine what "solved," "fair," or "presented to the player" means. The solver cannot fabricate a unique solution; the gate suite cannot pass a case that solves on a withheld clue; the bundle cannot ship an asset it did not really produce. Each refusal is enforced at a contract seam. This page is the contract-side companion to the V8 architecture set; the monolith hub is ../V8_ARCHITECTURE.md.
The defining structural fact — and the first thing to get right — is where the
contracts live. They are not in the central Zod contracts surface: there is
no libs/contracts/src/v8 directory (verified absent), and
the platform contracts page lists product
namespaces for v3, v6, and v9 but none for v8. V8's canonical shapes
are instead co-located in the domain libraries themselves, as plain
TypeScript interface/type declarations rather than zod schemas. The
self-contained, end-to-end-tested spine is the three libs/v8/* packages this
page is built around: @oshun/v8-case-csp (the constraint shapes + the prover),
@oshun/v8-case-gates (the case shape + the seven release gates), and
@oshun/v8-case-bundle (the forged, signed delivery bundle). That divergence
from V3/V6/V9 is deliberate, not an oversight, and the section below states it
plainly.
What ships, honestly#
Three things are true at once, and a reader is owed all three.
Implemented and hard-tested (real today). The three libs/v8/* packages are
genuine, composed, and exercised by specs that assert correctness, not just
shape. @oshun/v8-case-csp is a real finite-domain constraint solver —
backtracking search with node consistency, minimum-remaining-values ordering,
and forward checking (csp-solver.ts:212, solveCsp) — whose spec anchors
against the canonical Zebra puzzle answer, a known-correct result a stub
could not fake (the doc-comment at csp-solver.ts:12-16 states this, and
csp-solver.spec.ts proves it). The mystery layer's spec drives a
four-dimension murder case to a single named solution and rejects the
under-determined, over-constrained, and wrong-culprit variants
(mystery-skeleton.spec.ts:100-175). The gate suite spec asserts the registered
gate ids are exactly g1:fairness … g7:safety and walks each block path
(case-gates.spec.ts:30-114). The bundle spec C2PA-signs each generated asset,
verifies it, and confirms a tampered digest or uri fails verification
(case-bundle.spec.ts:123-167). These contracts compose three real shared
platform packages — @oshun/content-release-gates (ReleaseGateService,
gateFromEvalScore, gateFromManifestCheck), @oshun/content-quality-judge
(createGroundingGate), and @oshun/content-signing (the Ed25519
signer/verifier + sha256Hex) — so the gating and signing are not forked
per-product.
Provider-gated (fail-loud seams, not stubs). A case's surface — its prose,
suspect portraits, evidence images, music, voice-over — needs generation
providers. Those are injected boundaries, and their absence is reported, never
faked. With no prose writer wired, forgeCaseBundle throws
CaseBundleNotConfiguredError (case-bundle.ts:241-243) rather than inventing
a case; a media provider that does not support an asset kind yields a
not-configured asset slot carrying an honest reason, never fabricated bytes
(case-bundle.ts:259-267). The bundle's own doc-comment names the live
providers (Stability/Flux/Hunyuan3D/Meshy/Suno/ElevenLabs) as the [~]
remainder while the solvability proof, C2PA stamping, and gating are real
(case-bundle.ts:11-15).
Spec-vs-code drift, named honestly. The architecture monolith's §3 sketches
a different contract vocabulary — CaseSpec, CaseGroundTruth, the
MysterySession IR, and the FV5* V5 compile target — and sites them in
libs/yemaya/case-engine. Those richer shapes are real, but they live in a
second, independent package, @yemaya/case-contracts
(libs/yemaya/case-contracts/src/), and they are consumed by the §4 subsystem
family (@yemaya/case-engine, case-verifier, …), not by the libs/v8/* trio.
The two families share no imports in either direction (verified). The section
The two contract tracks reconciles them
so no reader is misled about which "ground truth" or "alibi" a given file means.
In particular, the first-class suspects and Alibi types the title implies
are named in @yemaya/case-contracts; in the tested libs/v8/* trio a suspect
and an alibi are modelled structurally, and that mapping is made explicit
below.
The case-bundle contract#
The terminal contract — what the pipeline actually emits — is the CaseBundle
(libs/v8/case-bundle/src/case-bundle.ts:156). It is the join of everything the
earlier stages produced into one gated, provenance-stamped object:
interface CaseBundle {
readonly case: V8Case; // the gated detective case
readonly uniqueness: CaseUniquenessResult; // the CSP verdict (proof, not claim)
readonly assets: readonly CaseAssetSlot[]; // generated | not-configured, per request
readonly evaluation: V8CaseGateResult; // the seven-gate report
readonly blocked: boolean; // evaluation.status === 'blocked'
readonly provenance: CaseBundleProvenance;
}
Two design choices in this shape carry the honesty of the whole product. First,
assets is a discriminated union, not an array of URLs (CaseAssetSlot,
case-bundle.ts:85-95): a slot is either
{ status: 'generated', request, manifest } or
{ status: 'not-configured', request, reason }. A consumer cannot read a uri
off a slot without first proving it generated — the type forces the
absent-provider case to be handled. Second, provenance is a real audit
record (CaseBundleProvenance, :146): it carries uniqueSolutionProven, a
solutionHash (the sha256 of the proven solution), the generatedAssetCount,
the explicit notConfiguredKinds, and the signer key id + public key PEM. The
solutionHash is computed over the actual proven assignment
(case-bundle.ts:288), so the provenance binds to the case that was solved,
not to a label asserting it was.
Each generated asset carries a C2paAssetManifest (:72-83): the
contentSha256 of the produced bytes, the signature algorithm, the signer key
id, and an Ed25519 signatureBase64 over a canonical manifest payload
(stable key order, manifestPayload at :166). verifyCaseAssetManifest
(:215) recomputes that canonical payload and checks the signature, so altering
any signed field — digest, uri, kind — flips verification to false; the spec
proves exactly this tamper detection (case-bundle.spec.ts:147-167). The forge
order itself is a contract invariant: forgeCaseBundle (:236) proves
uniqueness first and throws CaseNotSolvableError before any writer or media
provider is called, and the spec asserts no prose was generated for an
unsolvable skeleton (case-bundle.spec.ts:169-193). The full forge is the
subject of
./architectural-thesis-and-pipeline.md.
The constraint, clue, suspect & alibi shapes#
The case's skeleton — the half that must be machine-checked — is the
MysterySkeleton (libs/v8/case-csp/src/mystery-skeleton.ts:69):
interface MysterySkeleton {
readonly dimensions: Readonly<Record<string, readonly string[]>>; // each solution axis → its finite domain
readonly groundTruth: Readonly<Record<string, string>>; // the intended answer: one value per dimension
readonly clues: readonly MysteryClue[];
}
This is the structural answer to "suspects, alibis, the solution." A suspect
is a value in the culprit dimension's domain; the solution is the
groundTruth assignment over every dimension; and an alibi is a clue — an
is-not constraint that eliminates a suspect, or an allowed-tuple constraint
that ties a suspect to a place. A MysteryClue (:60) pairs a clueId, a
visibility of 'presented' | 'withheld' (only presented clues prove
solvability), and a MysteryConstraint — the load-bearing discriminated union
(:33-58) with six kinds: is, is-not, one-of, if-then, same, and the
general allowed escape hatch (an explicit set of permitted tuples over a scope
of dimensions). That last kind is the honest way to express an arbitrary fair
deduction "without hand-coding a predicate the data can't see" (:48-58); the
spec uses it to encode suspect→location access and suspect→motive maps
(mystery-skeleton.spec.ts:46-75).
Underneath sits a domain-agnostic CSP layer (csp-solver.ts): CspVariable
(:23), the CspConstraint union (:40 — equals, not-equals, in,
all-different, implies, and a general relation), and CspModel (:55).
compileMysteryToCsp (mystery-skeleton.ts:136) lowers a skeleton to a model,
including only presented clues by default — the model the player can
actually solve from. proveUniqueSolution (csp-solver.ts:356) finds up to two
solutions and returns a UniquenessResult (:343): unsatisfiable (0),
unique (1, with the witness), or under-determined (≥2). The mystery layer
wraps this into the contract every later stage reads, CaseUniquenessResult
(:211):
interface CaseUniquenessResult {
readonly uniqueSolutionProven: boolean; // the single flag the G2 gate consumes
readonly status: UniquenessStatus; // 'unique' | 'under-determined' | 'unsatisfiable'
readonly solutionCount: 0 | 1 | 2;
readonly solution?: Readonly<Record<string, string>>;
readonly matchesGroundTruth: boolean;
readonly reason?: string; // an honest one-line failure cause
}
The subtle, important field is matchesGroundTruth: proveCaseUniqueness
(:239) sets uniqueSolutionProven true only when the presented clues admit
exactly one solution and that solution equals the author's groundTruth. A
case whose visible clues uniquely prove a different culprit than the author
intended is a real generation bug, and the prover surfaces it (reason mentions
"ground truth") rather than asserting the case solved — the spec drives
precisely this butler-vs-doctor mismatch (mystery-skeleton.spec.ts:154-174).
The deep treatment of the solver and prover is
./clew-minos-palimpsest-symbolic-core.md.
The gate verdict shapes#
Between the skeleton and the bundle sits the gated case, V8Case
(libs/v8/case-gates/src/index.ts:52), and its clue, V8Clue (:42). These
are deliberately a different shape from the CSP MysteryClue: where the CSP
clue carries a logical constraint, the gate clue carries text, a
presentedBeforeReveal flag, and an optional redHerring marker — the
player-facing facts a fair-play checker needs. V8Case adds the
solutionClueIds (the clue ids the deductive chain depends on), the
uniqueSolutionProven flag (set from the CSP proof, never by hand), and three
0–100 scores (voiceDistinctivenessScore, proseScore, safetyScore).
evaluateV8Case (:183) registers seven gates on the shared
ReleaseGateService and returns a V8CaseGateResult (:173): a status of
'cleared' | 'blocked', the full ReleaseReport, and the blockedGateIds. The
seven gates (buildV8CaseGates, :99) are the contract's fairness invariants
made executable:
| Gate | Invariant (the contract refuses to ship a case that…) | Source of verdict |
|---|---|---|
g1:fairness |
…solves on a clue never presented to the player (every solutionClueId is shown) |
manifest check (:110) |
g2:solvability |
…has no proven unique solution (the §I.2 CSP flag) | manifest check over uniqueSolutionProven (:123) |
g3:clue-grounding |
…cites a clue absent from the case (invented evidence) | shared createGroundingGate (:130) |
g4:voice |
…has indistinct suspect voices (below minVoice, default 70) |
gateFromEvalScore (:139) |
g5:misdirection |
…has zero red herrings, or more than maxRedHerrings (an unfair maze) |
manifest check (:145) |
g6:prose |
…reads below the prose bar (minProse, default 70) |
gateFromEvalScore (:159) |
g7:safety |
…falls below the safety bar (minSafety, default 90) |
gateFromEvalScore (:165) |
g3 is the one that reuses platform machinery most directly: its retriever
(caseClueRetriever, :87) reports a solution clue id supported iff it
exists in the case's clue set and unsupported-claim otherwise, so a solution
that points at a non-existent "phantom-clue" is blocked as invented evidence
(case-gates.spec.ts:71-77). The thresholds are overridable via
V8CaseGateConfig (:70). The clearest contract guarantee here is the
cleared-iff-all-seven rule: status is 'cleared' only when the report
passed, and blockedGateIds lists every failing gate — the bundle's blocked
flag is just evaluation.status === 'blocked' (case-bundle.ts:296).
Referential integrity across the shapes#
The contracts are only canonical because their references resolve, and the
wiring between the three libs/v8/* packages is one acyclic chain — skeleton →
proof → case → bundle — with each edge a real value, not a duplicated literal:
- CSP proof → gate flag.
V8Case.uniqueSolutionProvenis never authored; it isCaseUniquenessResult.uniqueSolutionProvencarried forward.provenUniquenessFlag(mystery-skeleton.ts:290) exists precisely so a caller can set it from the proof without@oshun/v8-case-gatesimporting the CSP package (composition at the call site, the pattern the buildable-lib rules require). The mystery spec proves the end-to-end path: a CSP-proven case clearsg2, an under-determined one is blocked by it (mystery-skeleton.spec.ts:253-272). - Solution clues ⊆ case clues.
g1:fairnesschecks everysolutionClueIdresolves to apresentedBeforeRevealclue;g3:clue-groundingchecks everysolutionClueIdexists at all. Between them, asolutionClueIdsentry that dangles (no such clue) or that points at a withheld clue cannot pass. - Asset slot ⊆ requested assets. Every
CaseAssetSlotcarries its originatingCaseAssetRequest, andforgeCaseBundlewalksdraft.assetRequestsone-to-one (case-bundle.ts:258), so the bundle's asset list is exactly the writer's request list — each either fulfilled-and-signed or honestly not-configured. - Manifest digest → real bytes.
C2paAssetManifest.contentSha256is the sha256 of the bytes the provider actually returned (stampAsset,:184); the signature is over the canonical payload of those fields, so provenance binds to content, not to a promise of it.
The one referential boundary that does not exist is the cross-family one: the
libs/v8/* trio and @yemaya/case-contracts do not reference each other. That
is intentional — the tested vertical is self-contained — but it is also the seam
a reader must hold in mind, which is the next section.
The two contract tracks, reconciled#
V8 ships two real contract families, and they assign familiar names to differently-shaped objects. Telling them apart is exactly the kind of drift this page exists to flag (the V9 lesson-artifact contract page makes the same honest move for its dual gate schemes).
Track A — the tested vertical (libs/v8/*, scope:v8). MysterySkeleton /
MysteryClue / MysteryConstraint (case-csp), V8Case / V8Clue
(case-gates), CaseBundle / CaseAssetSlot / C2paAssetManifest
(case-bundle). Plain TS interfaces, no Zod, composing the shared platform
gate/sign packages. This is the track with the clean
skeleton→prove→gate→sign→bundle test wiring, and the one this page centres on.
Track B — the §3/§4 subsystem shapes (@yemaya/case-contracts). This
package realizes the architecture monolith's §3 vocabulary almost verbatim, as
plain TS interfaces across ten interface modules (plus a barrel, a generated V5
target, and a test): CaseSpec (case-spec.ts:18), CaseGroundTruth
(ground-truth.ts:156) — which, unlike the CSP skeleton, names
suspects: CharacterRef[], alibis: Alibi[], and
constraints: ConstraintClause[] as first-class fields — and a real Alibi
(ground-truth.ts:95) whose status is
'verified' | 'flawed' | 'unverifiable' with the culprit's flawed alibi
carrying exactly one breaking flawFactId. Its ir.ts does what §3.3 instructs
— it re-exports the existing validator's MysterySession/Clue/Solution
types rather than redefining them (ir.ts:40-54), and v5-target.ts holds the
FV5* compile target with a real deductionKindFromConfidence mapping
(v5-target.ts:31). So when the title says "suspects" and "alibis," the named
types live here; the proven logic lives in Track A. The two are parallel
realizations of one design, not rival implementations of one interface — and
neither lives in libs/contracts/src/v8, which does not exist.
The practical rule for a reader: if you are looking at the constraint solver, a
seven-gate report, or a C2PA-stamped bundle, you are in Track A (libs/v8/*);
if you are looking at CaseSpec difficulty bands, a named Alibi, the
MysterySession IR, or the V5 FV5* structs, you are in Track B
(@yemaya/case-contracts). The same word — "ground truth" — means a flat
Record<dimension, value> in the first and a CaseGroundTruth object with
suspects and alibis in the second.
Connections#
- The pipeline that fills these contracts, stage by stage: ./architectural-thesis-and-pipeline.md — the forge order, the repair loop, and why solvability is proven before generation.
- The symbolic core that produces the
CaseUniquenessResultand the proof theg2gate stands on: ./clew-minos-palimpsest-symbolic-core.md. - Why these shapes are co-located in the domain libs rather than the central Zod
surface — and what does live in
libs/contracts/(v3/v6/v9, not v8): the platform contracts page. - The architecture hub and the honest build status: ../V8_ARCHITECTURE.md.