Skip to main content

MEP 76. Mochi and Ruby gem bridge

FieldValue
MEP76
TitleMochi and Ruby gem bridge
AuthorMochi core
StatusDraft
TypeStandards Track
Created2026-05-29 22:15 (GMT+7)
DependsMEP-1 (Grammar, for the import ruby extension), MEP-2 (AST, for the import node), MEP-4 (Type System), MEP-13 (ADTs and Match, for optional / union translation), MEP-56 (Ruby transpiler, for the emit pipeline and the mochi-runtime gem), MEP-57 (Mochi module and package system, for mochi.toml, mochi.lock, the sparse-index protocol, the content-addressed object store, the capability declaration model, and OIDC trusted publishing)
Research/docs/research/0076/
Tracking/docs/implementation/0076/

Abstract

Mochi today (May 2026, after MEP-56's Ruby transpiler landed and MEP-57's source-level package system reached Draft) has ten code-emitting targets and a developing package management story. MEP-56 gave Mochi the ability to transpile to idiomatic Ruby source, and MEP-56 phase 22 shipped TargetRubyGem together with the mochi-runtime gem on RubyGems.org, proving that Direction 2 (Mochi-as-gem-producer) is already structurally complete. What neither MEP provides is Direction 1: a Mochi program cannot yet pull in an arbitrary gem from RubyGems.org, and there is no end-to-end mochi pkg publish --to=rubygems.org flow backed by OIDC trusted publishing. RubyGems.org hosts 170,000+ gems (May 2026); the top 100 most-downloaded gems account for hundreds of millions of downloads per month.

MEP-76 specifies the bidirectional Ruby gem bridge: Mochi packages can consume any gem via import ruby "<gem>@<semver>" as <alias> with no user-written boilerplate, and Mochi packages can publish to RubyGems.org as gems via mochi pkg publish --to=rubygems.org. The bridge is architecturally simpler than MEP-73 (the Rust bridge) in one critical dimension: because MEP-56 already transpiles Mochi to Ruby, there is no C FFI wrapper layer. The generated Ruby code simply calls require '<gem>' and invokes gem methods directly. The synthesised shim is a thin Ruby wrapper plus a Mochi extern fn corpus, not a foreign-language ABI bridge.

The proposal builds on MEP-57's manifest / lockfile / capability infrastructure and MEP-56's emit pipeline, and adds a new self-contained component under package3/ruby/. The Mochi grammar gains the single keyword ruby as a valid <lang> token in the existing FFI-import production. The lockfile gains one new repeated table ([[ruby-package]]). No existing transpiler MEP needs to change beyond what MEP-56 phase 22 already provided.

The system is anchored on seven load-bearing decisions, each justified in §Rationale: RBS as canonical API surface, direct require/call without a C FFI layer, gem_rbs_collection as fallback for gems without bundled RBS, a closed type-mapping table, TargetRubyGem reuse for Direction 2, RubyGems.org OIDC trusted publishing as the sole publish path, and Bundler as the build orchestrator.

Motivation

Mochi today integrates with foreign ecosystems through the Go, Python, TypeScript, and Rust FFI surfaces. Ruby is the fifth. The Ruby gap is distinct in character from the Rust gap (MEP-73) because Mochi already transpiles to Ruby: the bridge's consumer direction does not need to solve a cross-language ABI problem at all. The motivation has six dimensions:

  1. RubyGems.org is the canonical distribution channel for web application libraries, DevOps tooling, test frameworks, data-serialisation helpers, and cloud SDK adapters. Gems such as activesupport, faraday, redis, sidekiq, aws-sdk-s3, and sinatra have no equivalent in Mochi's standard library and no easy substitute in the other bridged ecosystems. A Mochi program targeting a Ruby web host (e.g., a Heroku dyno or a Fly.io app built with MEP-56's emit path) needs access to those gems without forfeiting the Mochi type system. The 170,000+ gems on RubyGems.org represent a decade of production-tested Ruby libraries that Mochi users should reach without forfeiting static typing.

  2. MEP-56's Ruby emit target makes the consumer direction uniquely cheap. Every competing FFI bridge (MEP-73 for Rust, the Go bridge, the Python bridge) requires either a C ABI layer or a subprocess protocol because the generated code runs in a different runtime from the foreign library. Mochi-to-Ruby output runs inside the Ruby runtime by definition: require 'gem_name' is the native Ruby mechanism, and the gem's Ruby code executes in the same MRI/CRuby or JRuby interpreter as Mochi's transpiled output. No shared-library linking, no subprocess IPC, no foreign-function ABI. MEP-76 exploits this structural advantage: the bridge's consumer shim is a file that calls require and a handful of delegation methods, not a synthesised extern "C" wrapper crate.

  3. RBS (Ruby 3.0+) has matured into a stable, machine-readable, schema-versioned type description for Ruby code. The Ruby core team shipped RBS as part of Ruby 3.0 (December 2020) and the format has been stable since. Gems that bundle their own RBS sigs store them under sig/ and declare them in the gemspec files list. The gem_rbs_collection repository (github.com/ruby/gem_rbs_collection) provides community-maintained RBS for the most popular gems that don't yet bundle their own. RBS gives the bridge a path to typed bindings that does not require parsing Ruby source, does not require running the gem, and does not require hand-written annotations on the gem author's side.

  4. The mochi-runtime gem on RubyGems.org is proof that Direction 2 already works. MEP-56 phase 22 shipped TargetRubyGem and pushed mochi-runtime (Apache-2.0, ~123 LOC) to RubyGems.org. The gem is live, versioned, and installable via gem install mochi-runtime. MEP-76 extends this proven infrastructure to user-written Mochi packages: any Mochi library can become a gem by adding a [ruby.publish] section to mochi.toml and running mochi pkg publish --to=rubygems.org. No new emit-pipeline work is needed on the MEP-56 side.

  5. RubyGems.org OIDC trusted publishing (GA 2023) is the current supply-chain best practice for the Ruby ecosystem. RubyGems.org launched OIDC trusted publishing in 2023 (stable GA, no opt-in flag required), using GitHub Actions id-token: write to obtain a short-lived OIDC token that the RubyGems.org server exchanges for a publishing certificate via Sigstore. This is the same pattern MEP-57 mandates for all publish flows. Long-lived GEM_HOST_API_KEY tokens are not accepted by MEP-76 for the same reasons MEP-73 rejects CARGO_REGISTRY_TOKEN.

  6. Bundler is the de-facto Ruby build orchestrator and the bridge should not reinvent it. Bundler has been the authoritative Ruby dependency resolver since 2010 and is bundled with Ruby itself since 2.6. Every production Ruby project already uses a Gemfile and a Gemfile.lock. MEP-76's build orchestration emits a Gemfile from the [ruby-dependencies] manifest table and delegates resolution and installation to bundle install. This means Mochi users get all of Bundler's network caching, platform-specific precompiled gem support, and lockfile semantics without the bridge having to re-implement a Ruby resolver. It also means a Mochi project with a pre-existing Gemfile can merge its Ruby dep graph with the Mochi-managed one via a bundle merge sub-step.

Specification

This section is normative. Sub-notes under /docs/research/0076/ are informative.

1. Pipeline overview

MEP-76 introduces a per-import Ruby dependency resolution layer that sits between the Mochi parser (after MEP-57 has resolved mochi.toml) and the MEP-56 build driver:

mochi.toml [ruby-dependencies]
| pkgmanifest.Parse + pkgsolver.Solve (MEP-57)
v
resolved Ruby dep tree (gem name + version + source URL)
| package3/ruby/sparseindex.Fetch (RubyGems sparse-index protocol)
v
.gem tarballs in ~/.cache/mochi/ruby-deps/<sha256-hex>/
| package3/ruby/rbs.IngestBundled (walk sig/ directory in gem)
| package3/ruby/rbs.IngestCollection (gem_rbs_collection fallback)
| package3/ruby/yard.IngestDoc (YARD doc fallback when no RBS)
v
RBS sig set (or YARD-derived pseudo-RBS) per gem
| package3/ruby/typemap.Translate (closed RBS-to-Mochi table)
v
TranslatedSurface + SkipReport per gem
| package3/ruby/shimemit.Emit (.rb shim: require + method wrappers)
v
ruby_shims/<gem>/shim.rb (emitted into MEP-56 output directory)
| package3/ruby/externemit.Emit (Mochi extern fn / extern type)
v
synthesised .mochi shim file per gem, imported by the user's source
| MEP-56 Driver.Build (TargetRuby / TargetRubyGem)
v
Ruby source tree or publishable gem

The bridge does not run the Ruby interpreter at ingest time. RBS file parsing is done entirely in Go (via the package3/ruby/rbs/ parser). The .rb shim is a thin delegation layer emitted as source by the Go side; the user's bundle exec ruby (or bundle exec rake build, for gem publish) runs it inside the Ruby runtime alongside the user's transpiled program.

2. Manifest extension: [ruby-dependencies], [ruby], and [ruby.publish]

The MEP-57 mochi.toml gains three new optional top-level tables:

[ruby-dependencies]
faraday = "~> 2.9"
redis = "~> 5.0"
sidekiq = ">= 7.2, < 8.0"
dry-types = "~> 1.7"
activesupport = { version = "~> 7.1", require = "active_support/all" }
my-local-gem = { path = "../my-gem" }
my-git-gem = { git = "https://github.com/example/my-gem", ref = "main" }

[ruby]
ruby-version = "3.3"
bundler-version = "~> 2.5"
platform = "ruby"

[ruby.publish]
gem-name = "my-mochi-package"
summary = "A Mochi package published as a Ruby gem."
description = "Longer description here."
homepage = "https://github.com/example/my-mochi-package"
license = "Apache-2.0"
authors = ["Mochi core"]
files = ["lib/**/*.rb", "sig/**/*.rbs"]
require-path = "lib/my_mochi_package"

[ruby-dependencies] follows RubyGems/Bundler's gem directive grammar: simple string for a version constraint, table for inline constraint plus require alias, path, or git/ref. This is intentional: Mochi users with Ruby background can copy entries from an existing Gemfile without translation; the bridge passes the table through to the synthesised Gemfile with a verbatim gem '<name>', '<constraint>' line per entry.

The [ruby] table holds Mochi-specific knobs:

  • ruby-version: the minimum Ruby version required. Written to Gemfile as ruby '~> <version>'. Default "3.0" (the Ruby 3.0 baseline where RBS is standard).
  • bundler-version: the Bundler version constraint. Written to Gemfile as gem 'bundler', '<constraint>'. Default "~> 2.5".
  • platform: the RubyGems platform string. Default "ruby" (pure-Ruby). Set to "x86_64-linux" etc. for platform-specific precompiled gems.

The [ruby.publish] table mirrors a gemspec. When present, mochi pkg publish --to=rubygems.org reads these fields to generate the .gemspec file that TargetRubyGem emits. All fields map one-to-one to Gem::Specification attributes. If the package already ships a hand-written .gemspec, the generated one is skipped and the hand-written one is used instead.

3. Lockfile extension: [[ruby-package]]

The MEP-57 mochi.lock gains one new repeated table:

[[ruby-package]]
name = "faraday"
version = "2.9.0"
source = { kind = "rubygems", registry = "https://rubygems.org" }
gem-sha256 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
rbs-source = "bundled"
rbs-sig-sha256 = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
shim-sha256 = "..."
capabilities-declared = ["net"]
dependencies = ["faraday-net_http@~> 3.0", "rack@>= 1.4", "logger"]

gem-sha256 is the SHA-256 of the .gem tarball as downloaded from https://rubygems.org/gems/<name>-<version>.gem. This hash is also the one RubyGems.org publishes in its index, enabling cross-verification.

rbs-source is one of "bundled" (RBS sigs shipped in the gem under sig/), "collection" (fetched from gem_rbs_collection), "yard" (derived from YARD doc), or "none" (no sig; the gem's entire surface is skipped with a SkipReport). A drift in rbs-source at mochi pkg lock --check time (e.g., the gem author added bundled RBS after a previous lock used "collection") is a warning, not an error, but the rbs-sig-sha256 must still match.

rbs-sig-sha256 is the SHA-256 of the concatenated RBS source files the bridge ingested (sorted by path, then concatenated). A drift here at --check time is a hard error: the shim must be regenerated.

shim-sha256 is the SHA-256 of the synthesised .rb shim file. A drift here at --check time is a hard error.

capabilities-declared mirrors MEP-57's capability model. The RubyGems bridge recognises net (gem opens network sockets), fs (gem reads/writes files), proc (gem spawns subprocesses), and native (gem includes a native C extension). The native capability gates phase 12 behaviour.

4. RBS-to-Mochi type mapping

The bridge uses a closed translation table. Items not in the table are skipped with a SkipReport entry naming the item and the reason.

RBS typeMochi typeNotes
Integerint
Floatfloat
Stringstring
SymbolstringSymbols become strings; SkipReport note if ambiguity with String
bool / boolish / true | falseboolRBS boolish is a common alias
nilnil literal
Array[T] where T in tablelist<T>
Hash[String, V] / Hash[Symbol, V] where V in tablemap<string, V>
T? (optional)T?Mochi optional
T | nilT?Normalised to optional
struct / Data classMochi recordFields must all be in-table
^(A) -> B (proc type, simple arity)fun(A): BOnly for fixed-arity, in-table A and B
Proc (untyped proc)refusedSkipReport: untyped proc
untypedrefusedSkipReport: untyped
selfrefusedSkipReport: self type
instancerefusedSkipReport: instance type
classrefusedSkipReport: class type
toprefusedSkipReport: top type
botrefusedSkipReport: bottom type
void (non-return position)refusedSkipReport: void in non-return
complex union A | B (neither is nil)refusedSkipReport: non-optional union

The closed-table philosophy is identical to MEP-73's: the bridge promises sound bindings for in-table items, not full generality. A user who needs an out-of-table item can write a hand-authored extern fn declaration in Mochi that takes responsibility for the type.

5. Surface syntax: import ruby "..."

The Mochi grammar's existing FFI-import production:

ImportStmt := "import" Lang? StringLit "as" Ident ("auto")?
Lang := "go" | "python" | "typescript" | "rust" | "ruby"

gains ruby as a Lang alternative. The string literal is one of:

  • <gem-name>: bare name, resolves through [ruby-dependencies] constraint plus mochi.lock.
  • <gem-name>@<semver-constraint>: explicit version constraint, must match [ruby-dependencies].
  • <gem-name>@git+<url>#<ref>: git source.
  • <gem-name>@path+<relative>: path source.

Example surface programs:

import ruby "faraday@~> 2.9" as faraday
import ruby "redis" as redis
import ruby "activesupport" as active_support

fn fetch_json(url: string): string {
let conn = faraday.new(url)
let resp = conn.get("/api/data")
return resp.body()
}

fn cache_set(key: string, value: string): bool {
let r = redis.new
r.set(key, value)
return true
}

The <alias> introduces a Mochi namespace. Symbol resolution looks up <alias>.<item> and binds against the synthesised extern fn declaration the bridge generated for <gem-name>#<method>. The auto keyword is admitted for import ruby ... auto to opt into flat top-level binding (rather than namespaced under the alias), matching the semantics already defined for import go ... auto.

6. Ruby shim emit

For each gem, the bridge emits two files:

ruby_shims/<gem>/shim.rb (injected into the MEP-56 emit directory):

# Auto-generated by mochi pkg lock. DO NOT EDIT.
# gem: faraday 2.9.0 (rbs: bundled)
require 'faraday'

module MochiShim__Faraday
def self.new(url)
::Faraday.new(url)
end

def self.get(conn, path)
conn.get(path)
end
# ... one method per translated RBS method signature
end

The shim module exposes every translated method as a module-level (self.) method, turning instance methods into two-argument calls (receiver + args). This is necessary because Mochi's extern fn model is procedural, not object-oriented: the Mochi caller passes the receiver object as the first argument.

ruby_shims/<gem>/shim.mochi (the extern fn corpus):

extern type Faraday__Connection

extern fn faraday__new(url: string): Faraday__Connection
extern fn faraday__get(conn: Faraday__Connection, path: string): Faraday__Response
extern fn faraday__response__body(resp: Faraday__Response): string
extern fn faraday__response__status(resp: Faraday__Response): int

The extern type declarations introduce opaque Mochi types for each gem class that appears as a receiver or return value in the translated surface. The external name mangling is <gem>__<class> for types and <gem>__<class>__<method> (or <gem>__<module_method> for module-level methods) for functions.

7. CLI surface

The Mochi CLI gains the following additions, all under the existing mochi pkg subcommand:

  • mochi pkg add ruby <gem>[@<constraint>]: adds an entry to [ruby-dependencies] and runs mochi pkg lock.
  • mochi pkg lock: as today (MEP-57), now extended to walk [ruby-dependencies], query the RubyGems sparse index for resolution, fetch each .gem tarball into the content-addressed cache, run RBS/YARD ingest, synthesise the shims, and write [[ruby-package]] entries.
  • mochi pkg lock --check: as today, now extended to verify gem-sha256, rbs-sig-sha256, and shim-sha256 for every [[ruby-package]] entry.
  • mochi pkg publish --to=rubygems.org [--dry-run]: builds the package as a Ruby gem via TargetRubyGem, packages it with gem build, obtains an OIDC token from the CI environment (GitHub Actions id-token: write), presents it to the RubyGems.org OIDC trusted-publishing endpoint, uploads the signed gem. --dry-run skips upload but still exercises the signing flow against a mock-rubygems harness.
  • mochi pkg sync ruby: regenerates the shim files from scratch (without changing the lockfile). Used after manual edits to the synthesised shim.

8. Build orchestration

When a Mochi program contains one or more import ruby "..." declarations, the MEP-56 build driver gains the following extensions:

  1. Before invoking bundle exec ruby (or rake build), the driver invokes package3/ruby/Bridge.PrepareWorkspace(workdir, mochiLock) which:

    • For each [[ruby-package]] in mochi.lock, materialises the gem source from the content-addressed cache.
    • Materialises the synthesised shim files into <workdir>/ruby_shims/<gem>/.
    • Writes a <workdir>/Gemfile that includes one gem '<name>', '<version>' line per [[ruby-package]] entry (exact version pinned from lockfile) plus gem 'bundler'.
    • Runs bundle install --deployment --frozen to install gems into <workdir>/vendor/bundle/ (the --frozen flag ensures the Gemfile.lock Bundler generates matches the mochi.lock pinned versions).
  2. The MEP-56 emit pass treats each import ruby "<gem>" as <alias> as a Mochi import "./ruby_shims/<gem>/shim.mochi" as <alias> shim, where the shim file is the extern fn corpus the bridge emitted.

  3. The emitted Ruby entry-point file (main.rb or lib/<name>.rb) gains a require_relative 'ruby_shims/<gem>/shim' line per imported gem.

  4. The driver invalidates the shim cache when any [[ruby-package]] shim-sha256 changes.

9. Publish flow (Direction 2)

mochi pkg publish --to=rubygems.org runs the following steps:

  1. Invoke TargetRubyGem (MEP-56 phase 22) to emit the gem source tree and .gemspec.
  2. Run gem build <name>.gemspec to produce <name>-<version>.gem.
  3. Obtain an OIDC token from the CI environment. On GitHub Actions this requires id-token: write in the workflow permissions block. The bridge checks for ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN environment variables and uses the GitHub Actions OIDC provider endpoint to obtain the token. On other CI providers (GitLab CI, Buildkite, CircleCI) the bridge uses the provider-specific OIDC endpoint.
  4. Present the OIDC token to https://rubygems.org/api/v1/webauthn_verification (the RubyGems.org trusted-publishing endpoint, GA 2023).
  5. RubyGems.org exchanges the token for a short-lived signing certificate via Sigstore/Fulcio and records a Rekor log entry.
  6. Upload <name>-<version>.gem to https://rubygems.org/api/v1/gems with the signing certificate.
  7. Record the Rekor log entry URL in mochi.lock under a new [publish] sub-table of the relevant [[ruby-package]] entry.

--dry-run executes steps 1-3, skips steps 4-7, and prints the token + payload that would be sent. A mock-rubygems harness (parallel to the sigstore-mock-fulcio harness in MEP-57) is available for integration test use.

Rationale

1. RBS as canonical API surface (vs YARD vs runtime introspection)

RBS is the official Ruby type system shipped with Ruby 3.0 (December 2020) and maintained by the Ruby core team. RBS files are machine-readable, schema-stable, and do not require running the gem. The alternative of parsing YARD documentation comments is fragile: YARD types are human-readable strings embedded in @param [Type] comments, not a formal grammar, and are routinely misspelled, omitted, or written as English prose. The alternative of runtime introspection (gem.instance_methods.map { |m| [m, gem.method(m).arity] }) provides method names and arity counts but no type information at all, which would make the closed-table philosophy impossible. A fourth alternative, parsing Ruby source ASTs via RuboCop-AST, would require a full Ruby parser in Go and would not handle method aliases, mixins, or modules forwarded via extend. RBS gives us formal types, class hierarchies, method signatures, and generic parameterisation in a stable, versioned format. The bridge treats RBS as authoritative and only falls back to YARD (phase 4) when no RBS is available at all.

2. Direct Ruby require/call, no C FFI layer

This decision is the fundamental architectural difference between MEP-76 and MEP-73. In MEP-73, the generated code runs inside the Mochi runtime (compiled to native via Rust), which is a different process-level runtime from the Rust crates the user imports. A C FFI wrapper crate is therefore necessary to cross the ABI boundary. In MEP-76, the generated code runs inside the Ruby interpreter (MRI or JRuby), which is the same runtime as every gem on RubyGems.org. require 'faraday' is the exact same mechanism that every Ruby script uses. There is no ABI boundary to cross, no extern "C" surface to synthesise, no box / unbox marshalling to write. The synthesised .rb shim is not an ABI bridge; it is a thin Ruby module that reorganises the gem's method signatures into the procedural shape that Mochi's extern fn model expects (receiver as first argument). The alternative of synthesising a C extension wrapper anyway (for "uniformity" with MEP-73) would add ~300 LOC of unnecessary complexity per imported gem and would break on JRuby, TruffleRuby, and mruby targets where C extensions do not run. Directness wins.

3. gem_rbs_collection as fallback for gems without bundled RBS

Many popular gems predate RBS (or have not yet added sig/ to their release process) but have community-maintained RBS sigs in the gem_rbs_collection repository (github.com/ruby/gem_rbs_collection), which is the official RBS community collection maintained by the ruby/rbs team. As of May 2026, gem_rbs_collection covers roughly 150 popular gems including activesupport, redis, sinatra, rack, faraday, rake, zeitwerk, and others from the fixture corpus. The bridge fetches the collection at mochi pkg lock time (pinning a specific commit SHA and recording it in mochi.lock), caches it in the content-addressed store, and queries it by gem name and version range before falling back to YARD. The alternative of requiring gem authors to add RBS to their gems before the bridge can import them would block 80%+ of the top 200 gems indefinitely. The alternative of skipping untyped gems entirely would make the bridge too narrow to be useful. The gem_rbs_collection fallback is the pragmatic middle path.

4. Closed type-mapping table (same philosophy as MEP-73)

The bridge promises typed Mochi bindings only for the subset of RBS types it can translate soundly. Items outside the table (complex union types, untyped procs, self, instance, class, top, bot, void in non-return position) are skipped with a structured SkipReport. This is the same closed-table philosophy as MEP-73's Rust bridge. The alternative of "best-effort" translation (e.g., mapping every complex union to any / object) would produce bindings that compile but silently violate type safety at runtime. The alternative of refusing to import any gem that has out-of-table items in its surface would block nearly every real gem (since virtually all Ruby libraries use untyped or self somewhere in their RBS). The closed table plus explicit skip-report gives the user a clear, auditable list of what the bridge translated and what it did not, without making unsound promises.

5. TargetRubyGem reuse for Direction 2

MEP-56 phase 22 already shipped TargetRubyGem: a build target that lowers a Mochi package to a publishable RubyGems gem, generates a .gemspec, and packages the mochi-runtime gem dependency. The mochi-runtime gem is already live on RubyGems.org (Apache-2.0, ~123 LOC), which means the gem push path (gem credentials, gemspec format, RubyGems.org API) has already been proven end-to-end. MEP-76 does not redesign TargetRubyGem; it extends it with OIDC trusted publishing metadata, reads publish metadata from mochi.toml's [ruby.publish] section, and wires the whole flow into mochi pkg publish. The alternative of designing a new TargetRubyGemBridge target for MEP-76 would duplicate code, fragment the publish path, and risk diverging the mochi-runtime gem from the user gem emit pipeline. Reuse wins.

6. RubyGems.org OIDC trusted publishing (GA 2023), no long-lived API key path

RubyGems.org launched OIDC trusted publishing in 2023 (stable GA, no opt-in flag), using GitHub Actions id-token: write to obtain a short-lived OIDC token. The RubyGems.org team published the trusted publishing spec and confirmed GA status in the RubyGems.org changelog for 2023. This matches the broader industry trend: npm (April 2024 GA), Maven Central (Sigstore GA October 2024), PyPI (PEP 740 GA late 2025), Cargo (RFC #3724 accepted Q4 2025) all converged on the same keyless OIDC pattern. MEP-76 ships exclusively on this path. The GEM_HOST_API_KEY environment variable path is not supported. The rationale is identical to MEP-73's rejection of CARGO_REGISTRY_TOKEN: long-lived API tokens are the leading cause of gem account compromise and supply-chain injection. The bridge was designed after those lessons; it does not repeat them.

7. Bundler as build orchestrator, not a custom resolver

Bundler has been the canonical Ruby dependency resolver since 2010, is bundled with Ruby 2.6+, handles platform-specific precompiled gem selection, understands platform gems (x86_64-linux, aarch64-linux, etc.), respects Gemfile.lock for reproducibility, and integrates with the full RubyGems.org API including the compact index. Writing a custom Ruby resolver in Go would be a major undertaking (estimated 8,000+ LOC to match Bundler's resolver fidelity on gems with complex gemspec constraint expressions) and would diverge from ecosystem tooling, breaking projects that mix Mochi-managed and hand-authored Gemfile entries. The bridge takes the same approach as MEP-57 takes with mochi.toml for Go: own the manifest layer, delegate resolution to the ecosystem's canonical tool. MEP-76 generates a Gemfile from [ruby-dependencies] and calls bundle install --frozen; the resulting Gemfile.lock is cross-checked against mochi.lock's [[ruby-package]] entries as a consistency assertion.

Phases

See /docs/implementation/0076/ for the per-phase tracking matrix. Fourteen phases cover skeleton (0), RubyGems sparse-index client (1), RBS bundled-sig ingest (2), gem_rbs_collection fallback ingest (3), YARD doc fallback ingest (4), RBS-to-Mochi type mapping (5), Ruby shim emit (6), import ruby grammar extension (7), Bundler build orchestration (8), mochi.lock integration (9), TargetRubyGem publish metadata (10), OIDC trusted publishing (11), native C extension gem handling (12), and Ractor/Fiber async bridge (13).

A phase is LANDED only when its gate is green against the curated 20-gem fixture corpus.

Target matrix

PhaseMRI Ruby 3.3 (darwin-arm64)MRI Ruby 3.3 (linux-x64)JRuby 9.4TruffleRuby 24
0. skeletonNOT STARTEDn/an/an/a
1. sparse-index clientNOT STARTEDn/an/an/a
2. RBS bundled-sig ingestNOT STARTEDn/an/an/a
3. gem_rbs_collection fallbackNOT STARTEDn/an/an/a
4. YARD doc fallbackNOT STARTEDn/an/an/a
5. type-mapping tableNOT STARTEDn/an/an/a
6. Ruby shim emitNOT STARTEDrequiredrequiredrequired
7. import ruby grammarNOT STARTEDrequiredrequiredrequired
8. build orchestrationNOT STARTEDrequiredrequiredrequired
9. mochi.lock integrationNOT STARTEDrequiredrequiredrequired
10. TargetRubyGem publish metadataNOT STARTEDrequiredn/a (publish is MRI only)n/a
11. OIDC trusted publishingNOT STARTEDrequiredn/an/a
12. native C extension gemsNOT STARTEDrequiredn/a (no C ext on JRuby)n/a
13. Ractor/Fiber async bridgeNOT STARTEDrequiredn/a (Ractor is MRI only)n/a

A phase marked n/a for a target is intentional: the bridge does not promise the behaviour on that target.

Non-goals

  1. Full Ruby source parsing. The bridge does not parse .rb files. It reads RBS files only. Ruby source parsing would require a full Ruby AST parser in Go and would not handle metaprogramming (method_missing, define_method, extend, include with dynamic module composition).

  2. Complete RBS coverage. Items outside the closed type-mapping table are skipped. The bridge does not attempt to translate arbitrary Ruby types to Mochi.

  3. Bidirectional method override / monkey-patching. The bridge generates read-only bindings: the Mochi caller calls gem methods, not the other way around. A gem cannot call back into Mochi code via the shim.

  4. Gem feature flags. RubyGems does not have a concept analogous to Cargo features. The require field in [ruby-dependencies] covers the common case of a gem with multiple require paths, but there is no gem-feature activation mechanism.

  5. mruby / mruby-on-WASM targets. MEP-56 targets MRI Ruby for the primary emit path. mruby is a distinct runtime with a different C API; gems that require MRI's C extension ABI do not run on mruby.

  6. Automatic RBS generation for the user's own Mochi package. When publishing as a gem (Direction 2), the bridge does not generate RBS sigs for the emitted gem's public surface. This is a future sub-phase (MEP-76 N.1) that requires a Mochi-type-to-RBS emitter.

Appendix: Fixture corpus

The 20-gem fixture corpus covers the top-downloaded RubyGems.org gems plus representative C-extension and async use cases.

GemVersion (May 2026)RBS sourcePhase gateNotes
json2.7.xbundled2Ruby stdlib-adjacent; always available
rake13.xcollection3Build tool; extensive module surface
rack3.xcollection3Web server interface
faraday2.9.xbundled2HTTP client
httparty0.22.xyard4No RBS; YARD fallback
redis5.xcollection3Key-value client
sinatra4.xcollection3Web DSL
rspec-core3.13.xyard4Testing; complex DSL surface
rubocop1.65.xbundled2Linter; large method surface
dry-types1.7.xbundled2Type system; record/sum types
zeitwerk2.7.xbundled2Code loader
dotenv3.xyard4Env file loader
activesupport7.1.xcollection3Rails utilities; large surface
nokogiri1.16.xcollection3C ext (libxml2); phase 12
pg1.5.xyard4C ext (libpq); phase 12
aws-sdk-s31.xbundled2Cloud SDK; async IO
openai2.xbundled2LLM API client
sidekiq7.xcollection3Background job; Ractor interest
puma6.xyard4Web server; C ext; phase 12
mini_magick5.xyard4Image processing via ImageMagick subprocess

Gems marked "C ext" (nokogiri, pg, puma) exercise phase 12 (native C extension gem detection and skip/warn/docker-prebuilt strategy). Gems marked "Ractor interest" (sidekiq) exercise phase 13 (Ractor/Fiber async bridge). Every gem in the corpus must produce a clean SkipReport (listing out-of-table items with reasons) and a compilable extern fn corpus before its phase gate is marked LANDED.

Acknowledgements

This MEP builds on MEP-56 (Ruby transpiler) for the emit pipeline, the TargetRubyGem build target, and the mochi-runtime gem on RubyGems.org; on MEP-57 (Mochi module and package system) for the mochi.toml manifest, the mochi.lock lockfile, the SHA-256 content-addressed object store, the OIDC trusted-publishing infrastructure, and the capability declaration scheme; on the Ruby core team for the RBS type system (Ruby 3.0+, stable since); on the ruby/gem_rbs_collection community for maintaining RBS signatures for popular gems; on Bundler for the dependency resolution model and the Gemfile / Gemfile.lock semantics; on the YARD documentation framework for the fallback type-extraction path; on RubyGems.org for the OIDC trusted publishing API (GA 2023); on Sigstore / Fulcio / Rekor for the keyless certificate and transparency log infrastructure; on the RubyGems sparse-index protocol for the dependency fetch path; on the Ruby on Rails core team for activesupport as a representative large-surface gem in the fixture corpus; and on MEP-73 (Rust bridge) for the architectural patterns (closed type-mapping table, capability declarations, lockfile dual-hashing, fixture corpus gate philosophy) that MEP-76 adapts to the Ruby context.