Ariadne · Architecture

Oracle Director, Cross-Cutting Concerns & Deployment

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

6sections14 minread1diagram

On this page

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-director is 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's POST /v8/cases/mint mints from a CaseSpec the 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-pipeline exports nine primitives, and runPipeline (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 in pipeline.test.ts. Two pieces are real-and-tested but not on the runtime path: the three-tier BudgetGovernor (§11.1) is exercised in tests (pipeline.test.ts:83) but runPipeline uses the per-case budget, and the reproduce determinism 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-localization is green (8 tests) but unimported by apps/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 whether clingo is 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 emits hashScope: 'locator' and signed: false; only a downstream signer that fetches the media bytes may upgrade those fields. runPipeline calls 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 default KeywordSafetyScanner (: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 leaves g6 = false and 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-increasing seq per (caseId, seed) run, and replayRun (:122) returns the run in seq order 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 hidden CaseGroundTruth so 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).

flowchart TD UE["V5/V6 UE5 client<br/>(V8_Ariadne_CaseClient plugin · spec)"] UE -->|"POST /v8/cases/mint · GET /v8/cases/:id"| LOOM subgraph svc["apps/v8 services (TS / node:http)"] LOOM["@v8/loom-service :4010<br/>runPipeline · buildOfflineDeps"] MINOS["@v8/minos-asp-sidecar :4070<br/>POST /solve · clingo? → DPLL fallback"] DAED["@v8/daedalus-compiler (CLI)<br/>case → FV5* + cold_cases_manifest.json"] end LOOM -->|"verify G1-G3"| MINOS LOOM -->|"publish"| DAED subgraph oracle["Oracle (lib · feeds specs · not yet wired)"] ORC["@yemaya/case-director<br/>player model · mint queue · placement"] end ORC -. "CaseSpec + warm queue" .-> LOOM subgraph xcut["@yemaya/case-pipeline (Stage 11 spine)"] BUD["budget + governor"] JRN["journal (seq)"] PROV["provenance (C2PA)"] SAFE["safety G6 (fail-loud)"] TEL["telemetry + drift"] end LOOM --- BUD & JRN & PROV & SAFE & TEL subgraph infra["Infra seams (in-memory default → prod swap)"] PG[("Postgres+pgvector<br/>journal store · canon")] RDS[("Redis<br/>event bus")] OBJ[("MinIO / S3<br/>generated assets")] end JRN -. store .-> PG JRN -. bus .-> RDS PROV -. assets .-> OBJ

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 exhaustedMintQueue.refill stops short with budgetLimited: true (mint-queue.ts:141); the lane is left partially warm. An over-ceiling spend clamps to headroom rather than overrunning (budget.ts:114), and the BudgetGovernor denies and names the blocking tier (:256).
  • Non-deterministic regenerationassertReproducible throws with the first divergent IR path (determinism.ts:188); the reproduce CLI exits non-zero.
  • Unscanned or flagged contentrequireSafetyClear throws, leaving G6 red and blocking publish (safety.ts:48, pipeline.ts:199).
  • Untranslatable localelocalizeStrings refuses untranslated text when no translator is configured (localize.ts:70); a logic-touching localization trips assertLocaleInvariant (:84).
  • Competing co-op writebacksarbitrateWriteback commits exactly the leader's outcome with a deterministic tiebreak (multiplayer.ts:38).
  • No clingo binary → the Minos sidecar reports clingo: false and falls back to DPLL for the same verdict (minos-asp-sidecar/src/server.ts:30).