Ariadne · Architecture

Canonical Data Contracts

A focused page within the Ariadne Architecture documentation. The full map and every sibling page live in the Architecture hub.

7sections12 minread1diagram1table

On this page
classDiagram class CaseSkeleton { +entities +domains +constraints +uniqueSolution } class NarrativeCase { +suspects +timeline +clues +redHerrings } class GateReport { +solvable +unique +fair +assetComplete } class CaseBundle { +manifest +provenance +signature } CaseSkeleton --> NarrativeCase : constrains dressing NarrativeCase --> GateReport : evaluated as GateReport --> CaseBundle : permits only on pass CaseBundle *-- CaseSkeleton CaseBundle *-- NarrativeCase

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:

ts
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):

ts
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 (:40equals, 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):

ts
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.uniqueSolutionProven is never authored; it is CaseUniquenessResult.uniqueSolutionProven carried forward. provenUniquenessFlag (mystery-skeleton.ts:290) exists precisely so a caller can set it from the proof without @oshun/v8-case-gates importing 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 clears g2, an under-determined one is blocked by it (mystery-skeleton.spec.ts:253-272).
  • Solution clues ⊆ case clues. g1:fairness checks every solutionClueId resolves to a presentedBeforeReveal clue; g3:clue-grounding checks every solutionClueId exists at all. Between them, a solutionClueIds entry that dangles (no such clue) or that points at a withheld clue cannot pass.
  • Asset slot ⊆ requested assets. Every CaseAssetSlot carries its originating CaseAssetRequest, and forgeCaseBundle walks draft.assetRequests one-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.contentSha256 is 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#