Skip to content

Give Result Effect's shape, encode snapshots, and add an Effect playbook - #58

Closed
SandroMaglione wants to merge 3 commits into
claude/gamepad-actionsfrom
claude/project-thread-m854ss
Closed

SandroMaglione wants to merge 3 commits into
claude/gamepad-actionsfrom
claude/project-thread-m854ss

Conversation

@SandroMaglione

Copy link
Copy Markdown
Member

Requested by Sandro · project thread

Makes Ritmo pair cleanly with Effect while keeping no dependency on it. Stacked on #57 (gamepad): merge that first, then retarget this to main.

Result has Effect's shape

  • A result is { _tag: "Success", success } | { _tag: "Failure", failure }, built with Result.succeed / Result.fail. It is the same plain data as Effect 4's Result, so Schema.decodeResult, decodeUnknownResult, and encodeResult plug into a descriptor's constructor as they are. Going the other way takes a two-line fromRitmo helper, because Effect's type carries a TypeId and pipe.
  • Every failure a host sees is tagged with _tag, so Effect.catchTag works:
    • from runtime.tick: SystemFailure, MissingRuntimeRequirements, TickOrderViolation
    • from FixedLoop: InvalidFixedLoopOptions, InvalidTickDelta, UpdateFailure
    • from Runtime.make: RuntimeConstructionError
    • from fromStandardSchema: StandardSchemaIssues
    • ritmo-math's errors (Vector2/Invalid, …), which used tag
  • A constructed descriptor's decode is now a known key, checked against its value. Without that, writing Schema.decodeResult(schema) and Schema.decodeUnknownResult(schema) inline left the value uninferred, and the code failed to compile. dtslint/Result.tst.ts covers this.
  • Clean break, no compatibility aliases. Most of the about 140 files changed are this rename, applied with a type-aware codemod.

Snapshots encode

  • A constructor can carry an optional encode (Descriptor.Encoder), the inverse of decode. runtime.snapshot() saves what it returns, so a Set, a Map, a class, or a transformed schema value (an Effect Option) survives JSON.stringify and restores. Values without one are saved as they are.
  • Decode.struct, array, and nullable carry the encode of the codecs inside them. fromStandardSchema(schema, { encode }) takes one.
  • Breaking: runtime.snapshot() returns a Result, failing with UnencodableComponent or UnencodableResource.
  • ritmo-devtools: a replay recorder skips a point the world cannot be saved at, and Replay.extract fails with SnapshotFailed.
  • oxlint-plugin-ritmo: handle-tick-result reports JSON.stringify(runtime.snapshot()) and structuredClone(runtime.snapshot()).

Effect playbook

packages/core/skills/ritmo/playbooks/effect.md, routed from SKILL.md. It covers:

  • importing Ritmo under an alias next to Effect
  • schemas as descriptors with Schema.toCodecJson
  • fromRitmo and catchTag
  • keeping Effect out of systems (resources, events, and synchronous services over a ManagedRuntime)
  • saving through Effect
  • owning FixedLoop.start and the input sources in a Scope

Its example was type-checked and run against Effect 4.0.2: an Option and a ReadonlySet round-trip through a snapshot.

Migration

The two changesets list each breaking fix as a diff. In short:

-if (!ran.ok) console.error(ran.error)
+if (ran._tag === "Failure") console.error(ran.failure)
-Result.success(x) / Result.failure(e)
+Result.succeed(x) / Result.fail(e)
-failure.kind === "SystemFailure"
+failure._tag === "SystemFailure"
-localStorage.setItem("save", JSON.stringify(runtime.snapshot()))
+const saved = runtime.snapshot()
+if (saved._tag === "Success") localStorage.setItem("save", JSON.stringify(saved.success))

Determinism.check's report and the trace events keep their own ok fields, because they are reports, not Results.

Checks

  • pnpm run check passes: typecheck, lint, 628 type tests, and 606 tests.
  • pnpm pack:check passes.
  • pnpm bench:check reports the same regressions on this machine for the base commit (Add Gamepad actions, and rebinding for keyboard and gamepad #57) as for this branch, so they come from the machine and not from this change.

Spirit Call moves to this once a release is published.


Generated by Claude Code

Ritmo's Result was { ok, value | error } while Effect's is
{ _tag, success | failure }, so every Effect game wrote the same
adapters, and Effect's Schema functions could not guard a descriptor
without wrapping each one. With the same plain fields, an Effect result
is a Ritmo result as it is, with no dependency on Effect. The tick,
Runtime.make, and Standard Schema failures now carry a _tag too, so
they narrow (and catchTag) like every other Ritmo error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwwnHMKTTiVBpH9SivGeLk
A snapshot saved the live value and the host serialized it, so a
component holding a Map, a Set, a class, or a value a schema library
transforms (an Effect Option) saved a shape its decode could not read
back. A constructor can now carry encode, the inverse of decode, and
snapshot() saves what it returns; values without one are untouched.
snapshot() returns a Result because an encoder can reject a value that
went into the world through typed commands. Effect's
Schema.encodeResult plugs in as an encoder as it is.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwwnHMKTTiVBpH9SivGeLk
The tick's MissingRuntimeRequirements and TickOrderViolation, FixedLoop's
failures, and ritmo-math's errors still used kind or tag, so a host
matching failures (or Effect's catchTag) saw two conventions.

A constructed descriptor's decode is now a known key, checked against its
value. Without it, writing Schema.decodeResult(schema) and
Schema.decodeUnknownResult(schema) inline in the constructor left the value
uninferred and failed to compile.

The ritmo skill gets a playbook for using Effect in the host.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwwnHMKTTiVBpH9SivGeLk
@SandroMaglione SandroMaglione self-assigned this Oct 22, 2026
@SandroMaglione
SandroMaglione deleted the branch claude/gamepad-actions October 22, 2026 10:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants