# Hephaestus — The Explorable Runtime

Hephaestus is the second of V9's three net-new structures (after the Atlas
concept graph): the runtime that turns a concept node into a **manipulable
surface whose state is _computed_, not animated**, and then proves the surface
is actually drivable to a defined target before it is allowed to ship. The whole
package — `@oshun/v9-hephaestus` — is deliberately tiny: three source files plus
an index
(`libs/v9/hephaestus/src/{nyx-sky-explorable,kalika-orbit-explorable,explorable-runtime}.ts`),
tagged `scope:v9, layer:domain, type:lib`, depending on exactly three workspace
packages (`@nyx/ephemeris`, `@oshun/contracts`, `@oshun/v9-lesson-explorables`).
That smallness is the point. Hephaestus introduces **no new physics and no new
ephemeris**; it is a typed binding layer that composes the real, already-shipped
Oshun kernels and attaches a _demonstrably-reached_ success state to each
binding. Its module doc-comment states the contract plainly: a P1 lesson must
"ship ≥1 computed-kernel explorable (a Nyx sky / Kalika sim), not a static
asset," and that explorable must have "a DEMONSTRABLY-REACHED success state
(`successState.reachable === true`), proven by the kernel output — a
generated-but-unverified explorable never ships"
(`libs/v9/hephaestus/src/explorable-runtime.ts:1`).

Why build it this way? Because an interactive widget is the easiest place in a
lesson to lie. A static diagram can be wrong; an _interactive_ diagram can be
wrong in motion — a slider that animates a plausible-looking but fictional sky,
a "physics" knob that tweens between hand-drawn frames. V9's answer is a hard
rule: the only explorables that count toward the lesson's Definition of Done are
ones where moving a parameter re-runs a real kernel, and where the `reachable`
flag on the success state was set by reading that kernel's output rather than
asserted by the generator. This makes the interactive claim of a lesson as
trustworthy as its grounded prose — both trace to a verifiable computation.
Hephaestus is consumed by the Prometheus forge at Stage 5 and feeds the **G4
completeness gate** (`libs/v9/prometheus/src/gates.ts:12`), so an explorable
that cannot prove its success state does not merely render a warning — it blocks
the lesson. This page is the deep companion to the architecture set; the
monolith hub is [../V9_ARCHITECTURE.md](../V9_ARCHITECTURE.md), and the
orientation page is [./overview.md](./overview.md).

## What ships, honestly

**Implemented and tested (real today).** The two computed-kernel explorable
builders are _actual computation_, not descriptions of computation. The Nyx sky
explorable integrates the genuine `@nyx/ephemeris` Sun position across a date
slider (`libs/v9/hephaestus/src/nyx-sky-explorable.ts:13`); the Kalika orbit
explorable runs the genuine `@kalika/symplectic` velocity-Verlet integrator over
a two-body Kepler system
(`libs/v9/lesson-explorables/src/orbit-explorable.ts:117`). The DoD/G4 runtime
(`evaluateExplorableDoD`, `explorableDoDToGateVerdict`,
`assertComputedExplorableDoD`) is real and wired into the Prometheus gate
composer. The `V9Explorable` contract is a real Zod schema with enforced
invariants. The hephaestus test suite (`hephaestus.spec.ts`) is **7/7 green**
and asserts _domain_ facts — the Sun's right ascension sweeps ≈ 360° over a
year, the symplectic integrator's relative energy drift stays below `1e-3` — not
just that objects are non-null.

**Real logic behind an injected boundary.** The third explorable family —
**generative widgets** for ideas without a kernel — lives in Theia, not
Hephaestus (`libs/v9/theia/src/generative-widgets.ts`). `verifyGenerativeWidget`
is real and correctly _fail-loud_: it refuses to mint an explorable unless a
`HeadlessWidgetRunner` reports both `compiled` and `reachedSuccess`, and it
throws `WidgetRunnerNotConfiguredError` when no runner is wired. But the
headless runner itself — the real browser/runtime check — is an injected
boundary; this repo does not bundle a browser harness, so the verifier ships its
policy, not a live sandbox.

**Spec-described / planned — corrected against the monolith.** Three monolith
claims are aspirational relative to what the code wires, and this page says so:

- **The `game-bridge` kind is types-only.** `V9ExplorableKindSchema` enumerates
  `game-bridge` (`libs/contracts/src/v9/explorable.ts:21`), but there is **no
  `game-bridge` builder anywhere in `libs/v9/`** — the only builders that exist
  are `buildNyxSkyExplorable`, `buildKalikaOrbitExplorable`, and the underlying
  `buildOrbitExplorable`. The V2-racing-as-applied-physics path is deferred, as
  the monolith itself admits.
- **The Kalika orbit computes in TypeScript, not WASM.** The §4 prose says the
  Kalika kernels are "compiled to WASM"; a real `wasm-bindgen` crate
  (`kalika-cas-wasm`) does exist under `libs/kalika/cas-engine`. But the shipped
  orbit explorable does **not** call it — it computes in pure TypeScript via
  `@kalika/symplectic`'s `integrateCanonical`. The result is correct-by-
  construction either way; the deployment story (WASM in a browser) is the part
  that is planned.
- **Hephaestus binds the ephemeris _compute_, not the planetarium _renderer_.**
  The "WebGL planetarium, 10M+ stars at 60fps" is the real, separate
  `libs/nyx/renderer` lib (a WebGPU renderer with WebGL2 fallback). Hephaestus
  imports only `@nyx/ephemeris` — the numeric Sun position — and never the
  renderer. A `V9Explorable` is a _binding + proof + provenance_, not a rendered
  canvas; the on-screen widget is a downstream delivery concern.

## The Explorable contract

The single source of truth is `V9ExplorableSchema`
(`libs/contracts/src/v9/explorable.ts:104`), the typed expression of the
monolith's
`{ conceptId, kind, params[], successState, groundingOrKernelRef, provenance }`.
Each field carries enforcement, not just shape:

| Field                  | Type                                                      | What it enforces                                                              |
| ---------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `conceptId`            | `V9ConceptId`                                             | The Atlas node this surface explores (`cn:<64-hex>` or `<namespace>:<slug>`). |
| `kind`                 | `computed-kernel` \| `generative-widget` \| `game-bridge` | The trust tier (below).                                                       |
| `title`                | 1–300 chars                                               | Human label.                                                                  |
| `params`               | `V9ExplorableParam[]` (max 32, default `[]`)              | The manipulable knobs; each `{min ≤ max, default ∈ [min,max]}`.               |
| `successState`         | `V9ExplorableSuccessState`                                | The reachable target (G4) — the heart of the contract.                        |
| `groundingOrKernelRef` | kernel-ref \| Sophia-pin union                            | Where the surface's truth comes from.                                         |
| `provenance`           | `{ contentHash, generator, c2paSigned }`                  | sha256 of the computed output + what produced it.                             |

Three invariants make the schema refuse to encode a dishonest explorable. First,
**parameter bounds keep the slider inside the verified regime**: the
`V9ExplorableParamSchema` `superRefine` (`explorable.ts:43`) rejects `min > max`
and a `default` outside `[min, max]`, so a knob can never be initialized into an
un-computed corner. Second, **a computed-kernel explorable must be backed by a
kernel, not a citation**: the schema-level `superRefine` (`explorable.ts:115`)
raises an issue if `kind === 'computed-kernel'` while
`groundingOrKernelRef.kind !== 'kernel'` — you cannot label a surface "computed"
and then ground it in a text pin. Third, and most important, **the success state
cannot be asserted blind**.

The `V9ExplorableSuccessStateSchema` (`explorable.ts:68`) is
`{ description, predicate, reachable, evidence? }`. The doc-comment is explicit:
"`reachable` is set true only after the success state was actually demonstrated
(by the kernel result or the headless verifier)." `predicate` is a
machine-checkable id (`"sunRaSweepDeg ≈ 360·days/yr"`,
`"energy.relativeDrift < maxEnergyDrift"`); `evidence` is the measured value
that made it pass (`"swept 360.9° (expected 360.0°)"`). The one helper the
contract exports, `isV9ExplorableShippable` (`explorable.ts:128`), is a
one-liner — `return ex.successState.reachable;` — and _every_ gate decision
routes through it. That tiny function is the seam where "we computed it and it
worked" is separated from "we hope it works."

The `groundingOrKernelRef` union (`explorable.ts:82`) discriminates on
`kind: 'kernel'` vs `kind: 'grounding'`. A kernel ref carries a `V9KernelRef`
(`libs/contracts/src/v9/primitives.ts:152`): a `kernel` drawn from the enum
`{nyx, kalika, nisaba, demeter}` plus a callable `ref` string like
`nyx:ephemeris#sun.position` and an optional `unit`. (Only `nyx` and `kalika`
have builders today; `nisaba` and `demeter` are reserved enum members.) The
`Lesson` artifact embeds this contract directly —
`explorables: z.array(V9ExplorableSchema).min(1).max(8)`
(`libs/contracts/src/v9/lesson.ts:209`) — so the "≥1 explorable" floor is a
schema constraint, while the stronger "≥1 _reached computed-kernel_ explorable"
rule is the Hephaestus DoD enforced at the gate.

## Three kinds — a trust gradient

The `kind` enum is ordered by **increasing generative risk**, and the runtime
treats the tiers very differently:

| Kind                | Trust        | Built by                                              | How the success state is proven                                                            | Status                         |
| ------------------- | ------------ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------ |
| `computed-kernel`   | P1 (highest) | `buildNyxSkyExplorable`, `buildKalikaOrbitExplorable` | A real kernel runs; `reachable` is read from the computed samples.                         | Shipped, 7/7 tests green       |
| `generative-widget` | P1/P2        | Theia `verifyGenerativeWidget`                        | A `HeadlessWidgetRunner` must report `compiled && reachedSuccess`; else blocked or throws. | Real verifier; runner injected |
| `game-bridge`       | P2/P3        | _(no builder)_                                        | Would reuse a V2–V8 substrate as a lab; not implemented.                                   | Types-only / deferred          |

Only the first tier satisfies the DoD. The runtime counts _computed-kernel_
explorables specifically (`explorable-runtime.ts:25`), so a lesson made entirely
of generated widgets still fails G4 until a kernel-backed surface is added. This
is the codified opinion that **the cheapest-to-fake surfaces earn the least
trust**.

## Computed-kernel explorables

### Nyx sky — "the wandering Sun"

`buildNyxSkyExplorable` (`nyx-sky-explorable.ts:61`) takes a cosmology concept
and a time-travel window (`startUnixMs`, `days` default 365, `samples`
default 366) and walks a date slider, calling the real `@nyx/ephemeris` kernel
at each step: `dateToJd(...)` then `calculateSunPosition(jd)` to get the Sun's
true apparent right ascension and declination. That ephemeris is not a toy —
`calculateSunPosition` computes Earth's heliocentric ecliptic position, takes
the antipode, and rotates ecliptic→equatorial with a time-dependent obliquity
(`libs/nyx/ephemeris/src/generator.ts:842`).

The success state is a **measured astronomical fact**: over the slider, the
Sun's RA should sweep forward by ≈ `360·days / 365.24219` (one tropical year per
full circle). The builder sums the forward RA deltas across all samples and
checks the result against the span-scaled expectation with a 10%-of-a-circle
tolerance:

```text
expectedSweep = 360 · days / TROPICAL_YEAR_DAYS
reachable = every sample finite  AND  |sweep − expectedSweep| / 360 < 0.1
```

Because the expectation scales with the span, a 30-day window honestly reaches
its _own_ scaled success state (~30°), not a hard-coded 360° — the test
`'does NOT reach … for a half-year span flagged as a full sweep'` exercises
exactly this, asserting a 30-day window sweeps 25–35° and is still `reachable`
(`hephaestus.spec.ts:40`). The explorable binds a single `dayOffset` slider over
`[0, days]`, grounds itself with
`kernelRef { kernel: 'nyx', ref: 'nyx:ephemeris#sun.position', unit: 'deg' }`,
and stamps provenance with a sha256 over the rounded RA/Dec series and generator
`'nyx:ephemeris'`. The returned `NyxSkyExplorable` also hands back the raw
`samples` and the measured `sunRaSweepDeg` — the time-series the renderer would
draw lives in that return value, _not_ inside the `V9Explorable` (the contract
stores the proof, not the trajectory).

### Kalika orbit — "a planet you can perturb"

`buildKalikaOrbitExplorable` (`kalika-orbit-explorable.ts:37`) wraps
`@oshun/v9-lesson-explorables`' `buildOrbitExplorable`, which is where the real
physics lives. That function constructs the 2-D Kepler gravity system as a
**separable Hamiltonian** `H = p²/2 − GM/|q|` (`orbit-explorable.ts:71`), starts
at perihelion `(r₀, 0)` with a purely tangential vis-viva speed
`v₀ = √(GM(1+e)/r₀)`, derives the bound-orbit period from the semi-major axis
`a = r₀/(1−e)` (Kepler's third law), and hands the system to
`integrateCanonical(..., { method: 'velocity-verlet' })` — the genuine
`@kalika/symplectic` geometric integrator (`orbit-explorable.ts:117`). It then
reads back the trajectory and the integrator's **backward-error energy
diagnostic** via `analyzeEnergyBehavior`.

The success state is the measured fact that a symplectic integrator _conserves
energy_ and the orbit _closes_:

```text
reachable = orbit.computed
          AND Number.isFinite(orbit.energyRelativeDrift)
          AND |orbit.energyRelativeDrift| < maxEnergyDrift   // default 1e-3
```

`orbit.computed` is itself honest — it is true only when the integrator produced
a finite, multi-sample trajectory (`orbit-explorable.ts:155`). The explorable
binds two sliders whose bounds keep the surface inside the verified regime:
`eccentricity` over `[0, 0.89]` step `0.01` (the underlying integrator rejects
`e ≥ 0.9`, so the slider's max stays safely below the singularity) and
`gravitationalParameter` (GM) over `[0.1, 10]`. It grounds with
`kernelRef { kernel: 'kalika', ref: 'kalika:symplectic#kepler.integrate' }`,
reuses the orbit's sha256 `contentHash`, and sets generator
`'kalika:symplectic'`. The test `'conserves energy on a circular orbit'` asserts
`|energyRelativeDrift| < 1e-3` and `reachable === true`
(`hephaestus.spec.ts:57`) — a test that would _fail_ against a hand-tuned or
random energy value, which is what makes it a real test rather than a shape
check.

## The DoD and the G4 gate

`explorable-runtime.ts` is the policy layer over those builders.
`evaluateExplorableDoD` (`explorable-runtime.ts:24`) filters a lesson's
explorables to the `computed-kernel` ones, then filters _those_ through
`isV9ExplorableShippable`, and returns a structured report:

```text
ExplorableDoDReport {
  ok: reachableComputedKernel > 0,
  total, computedKernel, reachableComputedKernel,
  reasons: [ "no computed-kernel explorable …", 'explorable "X" never reached its success state: …' ]
}
```

The `reasons` array is the _honest failure ledger_ — it names each unreached
explorable and quotes its success-state description, so a blocked lesson
explains itself. `explorableDoDToGateVerdict` maps the report to a
`V9GateVerdict` (`{pass, evidence}`); `assertComputedExplorableDoD` throws
`computed-explorable DoD not met: …` for callers that want a hard stop at build
time.

At Stage 7, `composeGates` (`libs/v9/prometheus/src/gates.ts:132`) makes the DoD
_be_ G4. It calls `evaluateExplorableDoD(input.explorables)` and emits the G4
verdict whose evidence reads, e.g.,
`"1/1 computed explorable(s) reached success state"`. The other six gates are
sourced elsewhere — G1/G2/G6 from Aletheia, G3 pedagogy and G5 quality and G7
provenance computed alongside — but **G4 is Hephaestus**. The lesson is
publishable only when all seven pass (`isV9LessonPublishable`,
`libs/contracts/src/v9/lesson.ts:246`), so a single unreached explorable blocks
delivery and caching.

The three gate-level tests close the loop: the DoD passes when a real Kalika
explorable reaches its state; it blocks an empty explorable list
(`assertComputedExplorableDoD([])` throws `/DoD not met/`); and it blocks an
explorable whose `reachable` has been forced to `false`, with `reasons[0]`
matching `/never reached its success state/` (`hephaestus.spec.ts:78`).

## Where it sits in the Prometheus pipeline

Hephaestus is Stage 5 of the nine-stage forge. The pipeline does not hard-code a
builder; it accepts an injected `explorableBuilder: (conceptId) => V9Explorable`
(`libs/v9/prometheus/src/pipeline.ts:76`) and invokes it on the concept the plan
chose to anchor (`pipeline.ts:147`). Today the forge ships exactly one
explorable per lesson (`explorables = [explorable]`), well under the contract's
max of 8. The end-to-end test wires the real Kalika builder as that seam
(`buildKalikaOrbitExplorable({ conceptId, orbit: { eccentricity: 0, steps: 360 }})`,
`pipeline.spec.ts:95`), so the green pipeline run is genuinely integrating real
symplectic physics into a gated artifact.

```mermaid
flowchart TD
    A["Stage 0–4: resolve · ground · plan · write · media"] --> B
    B["Stage 5 · Hephaestus<br/>explorableBuilder(conceptId)"] --> C
    C{kind?}
    C -->|computed-kernel| D["buildNyx / buildKalika<br/>run real kernel → samples"]
    C -->|generative-widget| E["Theia verifyGenerativeWidget<br/>HeadlessWidgetRunner"]
    D --> F["read kernel output →<br/>successState.reachable"]
    E --> F
    F --> G["evaluateExplorableDoD()<br/>count reached computed-kernel"]
    G --> H{ok?}
    H -->|yes| I["G4 verdict pass →<br/>composeGates → compile artifact"]
    H -->|no| J["G4 verdict fail →<br/>lesson BLOCKED, not cached"]
```

A worked trace for "how does a two-body orbit conserve energy?": Prometheus
resolves the wonder to a Kalika concept, the explorable builder runs
`buildKalikaOrbitExplorable`, which integrates 360 velocity-Verlet steps of a
circular orbit; the integrator's backward-error diagnostic reports a relative
energy drift around `1e-9`–`1e-12`; that is below the `1e-3` tolerance, so
`reachable` is set `true` with evidence
`relative energy drift 3.x e-10 (< 0.001); N samples`; `evaluateExplorableDoD`
counts `1/1` reached; G4 passes; the explorable — eccentricity and GM sliders,
kernel ref, content hash — is sealed into the `V9LessonArtifact`.

## Generative-widget verification (Theia)

For ideas with no kernel, the P2 path
(`libs/v9/theia/src/generative-widgets.ts:53`) verifies a generated widget
before it can clear G4/G5. `verifyGenerativeWidget` takes a
`GenerativeWidgetSpec` (the generated source, params, success description and
predicate) and a `HeadlessWidgetRunner`. It is structured to be impossible to
spoof: with no runner it throws `WidgetRunnerNotConfiguredError`
(`generative-widgets.ts:38`); with a runner, it returns
`{ shipped: false, reason }` unless the runner reports _both_ `compiled` and
`reachedSuccess`. Only then does it mint a `generative-widget` `V9Explorable`
with `successState.reachable: true`, a Sophia-style pin id derived from the
widget's content hash, and generator `'iris:widget-gen'`. The Theia spec
exercises all three outcomes — shipped on success, blocked when `reachedSuccess`
is false, and thrown when no runner is wired (`theia.spec.ts:92`). The boundary
is honest: the verifier is real; the browser that actually runs the widget is
the injected seam.

## Edge cases and failure modes

- **Validation is a throw, not a warning.** Both builders call
  `V9ExplorableSchema.parse(...)`, so a malformed param (default out of bounds)
  or a computed-kernel explorable mis-grounded with a pin raises a Zod error at
  build time. `buildNyxSkyExplorable` additionally guards `days > 0` and
  `samples ≥ 2` integer; `buildOrbitExplorable` guards `GM > 0`, `r₀ > 0`,
  `e ∈ [0, 0.9)`, `steps ≥ 2` integer.
- **Unreached ≠ thrown.** A _legitimately computed but failing_ surface (energy
  drift over tolerance, a NaN in the series) yields `reachable: false`, not an
  exception. That is correct: the explorable is real but not shippable, and the
  DoD reports it by name rather than crashing.
- **Empty / widget-only lessons block.** No computed-kernel explorable ⇒
  `report.ok === false` with the reason "a P1 lesson needs ≥1 Nyx/Kalika
  computed explorable." The contract's `min(1)` only requires _an_ explorable;
  the DoD requires a _reached computed-kernel_ one.
- **The contract stores proof, not data.** The `V9Explorable` carries the
  success evidence and a content hash, but the actual sample series lives in the
  builder's return type (`NyxSkyExplorable.samples`,
  `KalikaOrbitExplorable.orbit.samples`). A consumer that needs the trajectory
  must call the builder, not read the artifact — the artifact is a verifiable
  binding, kept small and cacheable.
- **`c2paSigned` defaults `false`.** Every builder emits `c2paSigned: false`;
  cryptographic signing is G7's job downstream, so an explorable straight out of
  Hephaestus is provenance-hashed but not yet signed.

## Configuration surface

There is little to configure, by design. The meaningful knobs are the builder
parameters: Nyx's `startUnixMs` / `days` / `samples` window, and Kalika's
`orbit` shape (`gravitationalParameter`, `perihelion`, `eccentricity`, `steps`)
plus a tunable `maxEnergyDrift` (default `1e-3`) that sets how strict the
energy-conservation success state is. Tightening `maxEnergyDrift` or raising
`steps` makes the success state harder to reach but the orbit truer; both are
honest trade-offs the caller owns. The pipeline-level seam is
`ForgeLessonInput.explorableBuilder`, which lets a deployment swap in a
different kernel binding without touching the gate logic.

## How it connects to neighbouring systems

Hephaestus sits in the middle of the V9 reuse stack: it _consumes_ substrate
kernels and _feeds_ the gate plane. Upstream, the **Atlas** graph supplies the
`conceptId` and (for STEM disciplines) the `kernelRef` that says a value must be
computed rather than stored — see
[./atlas-knowledge-graph.md](./atlas-knowledge-graph.md) and the Invariant-2
STEM rule in [./six-layer-reuse-stack.md](./six-layer-reuse-stack.md). Sideways,
it reuses the `@nyx/ephemeris` and `@kalika/symplectic` kernels exactly as
catalogued in
[./subsystem-map-and-reuse-ledger.md](./subsystem-map-and-reuse-ledger.md).
Downstream, the explorable is one field of the
[./lesson-artifact.md](./lesson-artifact.md) and its DoD _is_ G4 inside
[./seven-gates-and-aletheia.md](./seven-gates-and-aletheia.md); the Prometheus
forge that orchestrates Stage 5 is detailed in
[./prometheus-lesson-pipeline.md](./prometheus-lesson-pipeline.md). Finally, the
rendered, learner-facing surface and the cross-cutting provenance/caching
concerns are covered in
[./delivery-and-cross-cutting.md](./delivery-and-cross-cutting.md) — a reminder
that Hephaestus produces the _verified binding_, while drawing it on a real
WebGL canvas is a delivery-layer job.

## Related

- [V9 Architecture Overview](./overview.md) and the monolith hub
  [../V9_ARCHITECTURE.md](../V9_ARCHITECTURE.md)
- [Atlas — The Unified Knowledge Graph](./atlas-knowledge-graph.md) and the
  [Six-Layer Reuse Stack](./six-layer-reuse-stack.md)
- [Subsystem Map and Reuse Ledger](./subsystem-map-and-reuse-ledger.md) — the
  Nyx and Kalika kernels Hephaestus binds
- [Prometheus — The Lesson Pipeline](./prometheus-lesson-pipeline.md) (Stage 5)
  and [The Lesson Artifact](./lesson-artifact.md) (the embedded explorable)
- [The Seven Gates and Aletheia](./seven-gates-and-aletheia.md) — the DoD is G4
- [Delivery and Cross-Cutting Concerns](./delivery-and-cross-cutting.md)
