This is the operations page of the V8 (Ariadne) set — the part of the self-authoring detective universe that decides which minted case a player meets next, the disciplines that wrap every case regardless of which subsystem produced it, and the shape of the thing you actually deploy. The throughline is the same solve-first thesis viewed from the run-time side: once Clew has proposed a case and Minos has proved it fair and solvable, somebody has to keep a player in their flow channel, keep the bill bounded, keep every generated frame content-credentialed, keep a co-op session's canon coherent, and keep the whole thing reproducible from a seed. In V8 those are first-class, tested code — an Elo-style player model, a water-mark mint queue that throttles against a live budget, a clamp-to-headroom budget governor, an FNV-1a reproducibility key, a fail-loud safety seam, a leader-arbitrated canon writeback — composed beside the eight generation subsystems rather than hand-waved in a slide.
The concrete surface is three focused packages plus two HTTP services. The
Oracle is @yemaya/case-director: the drama manager that models the player,
paces difficulty, runs the mint-ahead queue, and places cases in the world
(Ariadne §10). The cross-cutting spine is @yemaya/case-pipeline: budget,
journal, determinism, provenance, safety, HITL, multiplayer arbitration,
telemetry, and a load harness (Stage 11). The localization/accessibility
pass is @yemaya/case-localization. The deployment is two node:http
services — @v8/loom-service (the orchestrator BFF the UE5 client reaches) and
@v8/minos-asp-sidecar (the constraint solver) — plus the
@v8/daedalus-compiler build tool. This page is the deep companion to the
monolith's §4.9 (Oracle), §5 (cross-cutting), and §6 (deployment shape); the
section hub is ../V8_ARCHITECTURE.md.
What ships, honestly#
The monolith describes an Oracle that reaches all the way into a running V5/V6 open world — seeding ambient mysteries onto live NPC schedules, keeping the Bureau Case Board warm, minting on demand at run time. The honest split is that the direction logic is real and tested, while its wiring into the live UE5 world and the loom-service run time is the open boundary.
- The Oracle is real and green, but it is a library, not yet a wired
runtime.
@yemaya/case-directoris a fully-implemented package — a genuine online-rating player model, streak-aware pacing with anti-repetition, a budget-throttled mint queue, deterministic placement, and a seasonal calendar — with 33 passing tests across five files, green when run directly with vitest. But no runtime app imports it yet: the Loom service'sPOST /v8/cases/mintmints from aCaseSpecthe caller supplies (apps/v8/loom-service/src/server.ts:40); the Oracle is precisely the component that would produce those specs and manage the warm queue. Treat the player model, pacer, and queue as tested decision logic, not a director already steering a shipped game. - The cross-cutting spine is real, and most of it is wired into the run
pipeline.
@yemaya/case-pipelineexports nine primitives, andrunPipeline(apps/v8/loom-service/src/pipeline.ts:127) threads five through every mint: the per-case budget (reserve/spend at each stage), the journal (a monotonic event per stage), telemetry, provenance, and the fail-loud safety gate — behaviour covered by 21 tests inpipeline.test.ts. Two pieces are real-and-tested but not on the runtime path: the three-tierBudgetGovernor(§11.1) is exercised in tests (pipeline.test.ts:83) butrunPipelineuses the per-case budget, and thereproducedeterminism check ships as a CLI (reproduce-cli.ts) rather than inside the mint. - Localization, accessibility, HITL, multiplayer arbitration, and the
difficulty-calibration feedback are real libraries with their own tests, not
yet called from the runtime apps.
@yemaya/case-localizationis green (8 tests) but unimported byapps/v8; the calibration the monolith says feeds the Oracle's player model (§9.4/§10) is a real function (calibrateDifficulty,libs/yemaya/case-eval/src/calibration.ts:24) whose drift is computed but whose write-back wire to the model is described, not coded. - Where something needs money, a model, or a binary, it is a fail-loud seam,
not a fake. A missing safety scanner refuses rather than passing content
through unscanned (
requireSafetyClear,safety.ts:48); a non-source locale with no translator refuses to emit untranslated text (localizeStrings,localize.ts:70); the Minos sidecar honestly reports whetherclingois present and falls back to the in-process solver (minos-asp-sidecar/src/server.ts:30). Honest "planned/gated" beats fake "shipped."
The Oracle: open-world case director#
The Oracle's job (monolith §4.9/§10) is to keep one player in the flow
channel — cases that are neither trivial nor impossible — while honoring their
taste, avoiding repetition, weaving their open story threads back in, and never
letting the mint bill run away. Its docstring states the remit precisely: model
each player (§10.1), keep a warm queue against a budget (§10.2), decide where
each case surfaces (§10.3), pace difficulty/theme (§10.4), and schedule the
seasonal drops that replace V5's authored packs (§10.5)
(libs/yemaya/case-director/src/index.ts:1).
The player model is a real online rating#
updateFromOutcome (player-model.ts:139) is the load-bearing function, and it
is a genuine Elo-style update, not a moving average of a label. Each finished
case carries an actual score in [0,1] — wrong→0, partial→0.45,
correct→0.8, brilliant→1 (OUTCOME_SCORE, :23) — and the model nudges the
skill estimate by K · (actual − expected). The expected term is the Elo
logistic (expectedScore, :118): each difficulty maps to an implied rating
(intro 0.25 … fiendish 0.9, DIFFICULTY_RATING, :111), so a player whose
skill matches the band is expected to score 0.5. The K-factor decays with
evidence (learningRate, :130): 0.12 + 0.4/(1+n) runs from 0.52 on a
player's first case to a ~0.12 asymptote, so provisional ratings find a player
fast and settled ones don't tank on one bad night. recommendedDifficulty
(:181) reads the estimate back into a band, and the model also keeps
theme/cell taste counters and weight-sorted open threads (e.g. "wrongly
accused Dockhand Mara, now freed") that pacing weaves into the next case.
Pacing: flow nudge, anti-repetition, canon weaving#
nextCaseSpec (pacing.ts:159) reconciles three pressures into one
deterministic CaseSpec. First, difficulty tracks skill but is nudged by the
recent streak: pacedDifficulty (:93) bumps one band up after a run of
strong solves (stretch) and one down after a run of weak ones (relief). Second,
anti-repetition: themeCandidates (:118) and cellCandidates (:139)
drop every theme and cell used in the last K cases from the pool — falling
back to the unfiltered pool only if exclusion would empty it, with the player's
tasted themes ordered first so favourites recur. Third, canon weaving: the
player's strongest open thread is written into
castConstraints.priorOutcomeHooks (:180) so the new case calls back to it.
Everything stochastic is driven by a seeded RNG, so a
(playerId, cell, counter) triple reproduces the same spec — the replay
requirement begins here.
The mint-ahead queue throttles against a live budget#
MintQueue (mint-queue.ts:83) keeps N verified cases warm per
(player, cell) lane so a player who walks up to the Case Board never waits on
the pipeline. refill (:131) is real water-mark scheduling: while a lane is
below its high-water mark it reserves the per-request cost before emitting
(DEFAULT_MINT_COST = 4000 tokens, 3 asset-jobs, :32), and the first
reservation the budget cannot grant stops the refill short — the lane is left
partially full and budgetLimited is set (:141). The Oracle thus throttles
its own mint rate against the remaining envelope rather than blindly minting.
The queue codes against the CaseBudget interface and accepts any conforming
budget: it ships a simple InMemoryCaseBudget (:219) for tests, but in
production the same lane logic runs over the three-tier governor below.
Placement and the seasonal cadence#
placeCase (placement.ts:86) decides where a verified case surfaces, in
priority order: a commissioned case (asked for via the Commission Studio) is
always honoured directly and never demoted; otherwise, if an active world
location matches the spec's required location and has a present NPC to carry the
hook, the case is seeded ambient into the living world; else it falls back
to the bureau Case Board. The selection is deterministic given the spec plus
the world snapshot. buildSeasonCalendar (seasonal.ts:86) replaces V5's
authored weekly cold-case packs with a generated drop calendar: weeks dated
drops seven days apart, each a deterministic CaseSpec, difficulty escalating
across the season (seasonalDifficulty, :59) and the cell rotating week to
week — and it never reads a clock, so a season is reproducible from its id. The
honest boundary: placeCase reads a WorldState interface; the live
NPC-schedule injection and the in-engine Case Board surface that consume its
decision are the UE5 plugin, the spec/external remainder.
Cross-cutting concerns (Stage 11)#
The cross-cutting spine is @yemaya/case-pipeline, whose index enumerates the
Stage-11 disciplines as named primitives (case-pipeline/src/index.ts:1). Each
is real, and the ones the mint touches are wired into runPipeline.
Budget and the three-tier mint governor#
createCaseBudget (budget.ts:170) composes the contracts' CaseBudget
surface over @yemaya/budget-management's ComputeBudgetManager. Its
load-bearing invariant is no silent overrun: spend (:104) clamps a
commit to the remaining headroom so committed spend can never exceed the ceiling
— over-budget means upstream degrades (fewer bespoke assets), never a quiet
overshoot — while reserve (:78) is a soft hold two parallel stages cannot
jointly push past the ceiling. Above per-case budgets, BudgetGovernor (:215)
enforces per-case ∧ per-player ∧ global ceilings at once: authorize
(:242) admits a spend only if it fits all three and names the first failing
tier, and throttle (:290) answers whether the Oracle may admit one more mint
under the player+global envelopes — the exact signal the mint queue honours. The
governor is tested (pipeline.test.ts:83), but runPipeline uses the per-case
budget today; the three-tier envelope is the production wrap.
Determinism and replay#
A case is reproducible from (seed, model-versions, canon-snapshot-hash) — both
a provenance guarantee and a cost lever (a popular case is minted and gated
once, served many times). canonicalize (determinism.ts:36) produces a
structurally-stable, JSON-safe view — Maps become key-sorted pair arrays,
Sets sort, and wall-clock fields are dropped — so the IR identity excludes
timestamp noise. reproKeyOf (:119) folds the seed, provider-sorted model
versions, and canon hash into an FNV-1a-64 key; assertReproducible (:188)
re-runs the engine and throws with the first divergent IR path if two runs
disagree (a real replay-failure signal, not a swallowed boolean). This ships as
the reproduce CLI (reproduce-cli.ts), which regenerates a case twice and
proves the IR bit-for-bit identical or exits non-zero.
Provenance, safety, journal, HITL, and multiplayer#
- Provenance metadata.
recordProvenance(provenance.ts) binds a published case to its repro key and captures every asset's claim handle, generator, content hash,hashScope, and signed state from the manifest (not a sample), with minted/reused counts. Loom currently emitshashScope: 'locator'andsigned: false; only a downstream signer that fetches the media bytes may upgrade those fields.runPipelinecalls the recorder on every mint. The generated field boundary is Asset Provenance Fields. - Safety (G6).
requireSafetyClear(safety.ts:48) is fail-loud by construction — it throws both when no scanner is configured (refusing unscanned content) and when the scanner reports flags. The offline defaultKeywordSafetyScanner(:123) is a genuine word-boundary scan over a curated abuse/defamation/self-harm lexicon, never always-clear; the pipeline wraps it so any throw leavesg6 = falseand blocks publish (pipeline.ts:199). - Journal.
StageJournal.emit(journal.ts:88) writes each stage transition to a durable store and a live bus, stamping a strictly-increasingseqper(caseId, seed)run, andreplayRun(:122) returns the run inseqorder for audit. The in-memory default satisfies both interfaces; Postgres + Redis are the production swaps. - HITL & multiplayer.
InMemoryHitlQueue(hitl.ts:61) is a real pending/resolved queue for the three escalation reasons (repair escalations, commissioned cases, flagged content), each item carrying the hiddenCaseGroundTruthso a reviewer sees what the machine saw.arbitrateWriteback(multiplayer.ts:38) resolves competing co-op outcomes: the session leader's proposal wins, with a deterministic seq-then-playerId tiebreak so the committed canon is reproducible regardless of network arrival order.
Localization and accessibility#
localizeStrings (localize.ts:63) keeps the deduction logic
locale-invariant — only display strings (each with a stable id from the
writers' room) are translated, never the symbolic IR — so a case solves
identically in every locale, and assertLocaleInvariant (:84) fails if a
locale bundle drops or invents a string id. A live translator is an injectable
fail-loud seam; PseudoLocaleTranslator (:36) covers the offline i18n path.
The accessibility pass derives everything from real case structure:
generateCaptions (accessibility.ts:27) emits a speaker-tagged caption per VO
line; assignEvidenceCues (:68) gives each clue category a distinct shape +
fill pattern so evidence is distinguishable without colour; and
generateHints (:87) builds progressive hints straight from the
solution's deduction path — each level reveals one more step — so a stuck player
is helped without being handed the answer.
Telemetry and drift#
TelemetryAggregator (telemetry.ts:63) records how shipped cases actually
play — solve-rate, the Brilliant/Good/Doubtful/Wrong rating distribution, an
abandon-point histogram — and driftAlert (:136) fires when solve-rate or
mean quality regress past a tolerance below a baseline, so a bad cohort of
generated cases is caught before it spreads (runPipeline records a session per
mint, pipeline.ts:248). The companion load harness mintThroughput
(loadtest.ts:67) measures sustained cases/second at real concurrency against a
monotonic clock (refusing a wall-clock fallback), throwing if a
minCasesPerSecond SLA is missed.
Deployment shape#
V8 deploys as TypeScript over the shared Oshun infrastructure: three small
node:http/Nest-style services and the subsystem libraries they compose. The
client never holds provider keys — it reaches everything through the Loom relay
(monolith §6).
The services. @v8/loom-service is the orchestrator BFF
(createLoomServer, server.ts:25): POST /v8/cases/mint runs the full
pipeline and returns the compiled pack only if all eight ReleaseDecision
gates pass (HTTP 200 vs 422 with blocking reasons), GET /v8/cases/:id fetches
a minted pack, and /health reports liveness (default port 4010). Its
buildOfflineDeps (deps.ts:44) is the zero-credential path the §12.4
acceptance run exercises — the deterministic Clew core, a text-only asset pack,
the local KeywordSafetyScanner, and in-memory budget/journal/telemetry — with
the production deps swapping in a live LLM proposer, the real Isis asset
realizer, the V7 Sekhmet scanner, and canon-graph retrieval.
@v8/minos-asp-sidecar (server.ts:18) is the constraint solver as a
dependency-light container: POST /solve returns the verdict and /health
reports whether a clingo backend is available, falling back to the in-process
DPLL solver for an identical verdict (default port 4070).
@v8/daedalus-compiler is the build-time CLI that compiles a green case into
V5's FV5* Mind-Palace structs plus a cold_cases_manifest.json.
The infrastructure seams. Every stateful dependency is an interface with a
real in-memory default and a documented production swap: the journal's
JournalStore/EventBus default to one InMemoryJournal and swap to
Postgres + Redis; the per-case budget defaults to an in-memory manager;
generated assets land in MinIO/S3; the canon graph is Postgres +
pgvector with an optional Neo4j overlay. Build targets are honest about
scope — the service build is a tsc --noEmit typecheck and the services
self-execute under node; the heavy infra (the clingo image, ACE/Inworld
run-time interrogation with the new OSHUN_INWORLD_* / OSHUN_NVIDIA_ACE_*
credentials, and the V8_Ariadne_CaseClient UE plugin) is the external
remainder the monolith enumerates as tasks. The deeper compile target and eval
path are on
./daedalus-compiler-and-theseus-eval.md.
Failure modes and refusals#
The layer is built to refuse rather than fabricate, and each refusal is specific and located:
- Mint budget exhausted →
MintQueue.refillstops short withbudgetLimited: true(mint-queue.ts:141); the lane is left partially warm. An over-ceilingspendclamps to headroom rather than overrunning (budget.ts:114), and theBudgetGovernordenies and names the blocking tier (:256). - Non-deterministic regeneration →
assertReproduciblethrows with the first divergent IR path (determinism.ts:188); thereproduceCLI exits non-zero. - Unscanned or flagged content →
requireSafetyClearthrows, leaving G6 red and blocking publish (safety.ts:48,pipeline.ts:199). - Untranslatable locale →
localizeStringsrefuses untranslated text when no translator is configured (localize.ts:70); a logic-touching localization tripsassertLocaleInvariant(:84). - Competing co-op writebacks →
arbitrateWritebackcommits exactly the leader's outcome with a deterministic tiebreak (multiplayer.ts:38). - No clingo binary → the Minos sidecar reports
clingo: falseand falls back to DPLL for the same verdict (minos-asp-sidecar/src/server.ts:30).
Related#
- The section hub: ../V8_ARCHITECTURE.md
- ./architectural-thesis-and-pipeline.md — the solve-first thesis and the eleven-stage pipeline whose stages the journal and budget thread through
- ./daedalus-compiler-and-theseus-eval.md — the compile-to-V5 target and the Theseus playtester whose difficulty calibration feeds the Oracle's player model
- ./canonical-data-contracts.md — the
CaseSpecthe Oracle emits and theCaseGroundTruththe HITL queue carries - ../../platform/overview.html — the shared Oshun platform (budget management, content signing, release gates) V8 composes rather than re-implements