CyberTrainУчебник

Начало работы: блог

Повторяет Getting Started самого Rails: блог со статьями и комментариями. Вы создадите скаффолд статей, добавите валидации, вложите в статьи комментарии, напишете тест, который прогоняет приложение внутри процесса, и соберёте приложение в готовый к развёртыванию каталог dist/. Готовое приложение — examples/blog; оно проверяется в CI на Linux и macOS.

Что нужно
Ruby 3.2+, cc, make, git, curl, заголовки SQLite 3 (Spinel 2026.09.12 установится сам) Шаг 00
Начало
gem install cybertrain
Результат
dist/blog

Вы здесь:Подготовка Ruby, cc, SQLite

Требования

ЗачемCyberTrain компилируется через Spinel в C, поэтому ему нужны инструменты C и заголовки SQLite; команда cybertrain — это гем, поэтому ей нужен Ruby.

  • Ruby 3.2+ и RubyGems — только для команды cybertrain; приложения никогда не запускаются на CRuby.
  • Spinel 2026.09.12; с другими версиями не тестировался. Вручную его устанавливать не нужно: первая команда, которой он нужен, собирает этот релиз в ~/.cybertrain (несколько минут, один раз), если spinel этого релиза ещё нет в PATH.
  • Инструменты C (cc или компилятор, указанный в CC) и make, а также git и curl — для сборки Spinel, который компилирует каждую программу в C.
  • Заголовки и библиотека SQLite 3: libsqlite3-dev в Debian/Ubuntu; на macOS входят в Xcode command line tools.
  • Заголовки OpenSSL (libssl-dev) необязательны: без них Spinel собирается без своего пакета openssl, который CyberTrain не использует.

cybertrain doctor проверяет эти инструменты и заголовки, а также установку Spinel; если чего-то обязательного не хватает, она также выводит команду установки для вашей платформы.

Как CyberTrain устанавливает SpinelЧто происходит, когда команде впервые нужен spin

cybertrain new, db, server, build и spin сначала ищут Spinel 2026.09.12: spinel и spin этого релиза, уже доступные в PATH; иначе — копию в ~/.cybertrain/spinel/2026.09.12/; иначе собирают эту копию из релизного тега Spinel (git clone, make deps, make -j, make install) — один раз, за несколько минут. Лог пишется в ~/.cybertrain/log/spinel-2026.09.12-build.log; если не хватает инструмента, нужного для сборки, cybertrain ещё до её начала сообщает, какого именно, и выводит команду установки для вашей платформы.

cybertrain setup делает то же самое по запросу (--force пересобирает копию в ~/.cybertrain) и, если используется именно её копия, выводит строку, добавляющую её в ваш PATH, чтобы запускать spin самостоятельно:

терминал — любой каталогshell
export PATH="$HOME/.cybertrain/bin:$PATH"

В ~/.cybertrain/bin лежат ссылки spinel и spin на используемый релиз. Если этого каталога нет в PATH, cybertrain spin ARGS... запускает spin этого релиза из любого каталога. CYBERTRAIN_HOME заменяет ~/.cybertrain, а CYBERTRAIN_SPINEL_HOME=PREFIX вместо этого использует Spinel, установленный в PREFIX (это должен быть тот же релиз). spinel другого релиза в PATH никогда не используется и не затрагивается.

Негде устанавливать? Песочница открывает этот блог в GitHub Codespace, где шаги 02, 03, 04 и 07 уже выполнены.

Вы здесь:Подготовка CLI cybertrain

Установка CLI

Зачемcybertrain — команда, которая создаёт приложения, генерирует скаффолды ресурсов, выполняет миграции, запускает сервер и собирает.

Первая строка устанавливает её из RubyGems. Остальное — весь учебник в двух словах; шаги ниже разбирают его по одной команде.

терминал — любой каталог, затем blog/shell
gem install cybertrain
cybertrain new blog          # при первом запуске устанавливает Spinel 2026.09.12 (несколько минут)
cd blog
cybertrain g scaffold article title:string body:text
cybertrain db migrate
cybertrain server            # http://127.0.0.1:3000
cybertrain build             # продакшен: dist/blog, dist/public/, dist/storage/, dist/tmp/

Команда cybertrain написана на обычном Ruby, и гем запускает её под CRuby (3.2 или новее); ни фреймворка, ни Spinel в геме нет. Её команды (их выводит cybertrain help):

  • cybertrain new NAME [--path DIR | --version V | --git URL [--ref R]] [--skip-spin] создаёт приложение в NAME, затем запускает в нём spin lock и spin run gen, при необходимости сначала устанавливая Spinel.
  • cybertrain generate scaffold NAME field:type ... (псевдоним g scaffold) добавляет ресурс в приложение в текущем каталоге.
  • cybertrain db migrate выполняет генерацию, применяет ожидающие миграции и генерирует снова (spin run gen; spin run db -- migrate; spin run gen); cybertrain migration оставлена как псевдоним.
  • cybertrain db COMMAND... после spin run gen запускает любую другую команду базы данных: status, rollback [N] (после которой генерация выполняется снова), schema:dump, create.
  • cybertrain server [PORT] выполняет генерацию, затем запускает сервер разработки (spin run gen; spin run NAME или spin run NAME -- PORT; порт по умолчанию — 3000).
  • cybertrain build собирает NAME со встроенными app/views/ и формирует dist/ (бинарный файл, public/, storage/, tmp/).
  • cybertrain spin ARGS... запускает spin с зафиксированным Spinel из любого каталога: cybertrain spin test, cybertrain spin run gen -- --check.
  • cybertrain setup [--force] устанавливает Spinel 2026.09.12 в ~/.cybertrain, если spinel этого релиза ещё нет в PATH; --force пересобирает копию в ~/.cybertrain.
  • cybertrain doctor проверяет инструменты C, заголовки SQLite и установку Spinel.
  • cybertrain version выводит версию (cybertrain 0.2.1); cybertrain help выводит справку по использованию.

db, server и build запускаются внутри приложения; NAME — это [package] name из его spin.toml.

Вы здесь:Вы пишете blog/

Создание приложения

ЗачемВы получаете привычную для Rails структуру каталогов и три программы в bin/: bin/blog.rb (приложение), bin/gen.rb (генератор) и bin/db.rb (миграции при разработке).

терминал — любой каталогshell
cybertrain new blog
cd blog

cybertrain new создаёт приложение, чей spin.toml указывает на релиз, соответствующий CLI:

spin.tomltoml
[package]
name = "blog"
version = "0.1.0"

[dependencies]
cybertrain = { git = "https://github.com/saeki-mototsune/cybertrain", ref = "v0.2.1" }

Затем она проверяет, что Spinel установлен (в первый раз это означает сборку Spinel 2026.09.12 в ~/.cybertrain: несколько минут, один раз; см. шаг 00), и запускает spin lock (spin загружает фреймворк в свой кэш ~/.cache/spin/packages/ и фиксирует коммит в spin.lock) и spin run gen — примерно так же, как rails new запускает bundle install, — так что cybertrain server сразу работает. В конце она выводит следующие шаги. Ничего из фреймворка в приложение не копируется; закоммитьте spin.toml и spin.lock.

Если не удаётся установить Spinel или выполнить один из двух шагов spin, файлы остаются, new завершается с кодом 1 и выводит, что запустить после устранения причины (для Spinel: cybertrain setup, затем cybertrain spin lock && cybertrain spin run gen в новом каталоге). --skip-spin только записывает файлы; --path DIR вместо этого делает зависимостью локальную копию репозитория, --git URL [--ref R] — фреймворк по адресу URL (на ветке или теге R), а --version V — версию из индекса, когда cybertrain появится в spin-index.

Имена приложений состоят из строчных букв, цифр и _ и начинаются с буквы. Сгенерированное дерево:

blog/text
blog/
  spin.toml  config/app.rb  config/routes.rb
  db/schema.rb  db/migrate/
  app/controllers/application_controller.rb  app/models/  app/views/  app/helpers/
  public/  bin/blog.rb  bin/gen.rb  bin/db.rb
  gen/  storage/  tmp/  test/

bin/blog.rb — единственная точка входа приложения, программа, которую компилирует spin build blog: ./blog обслуживает запросы, ./blog migrate и ./blog db status управляют базой данных. bin/gen.rb запускает генератор (spin run gen), а bin/db.rb запускает мигратор во время разработки (его использует cybertrain db migrate, потому что bin/blog.rb не скомпилируется, пока первая миграция не сгенерирует gen/models/). В gen/ уже лежит views.rb — закоммиченная пустая таблица встроенных представлений.

ApplicationController изначально выглядит так, поэтому отсутствующая запись через rescue_from превращается в 404:

app/controllers/application_controller.rbruby
class ApplicationController < Cybertrain::Controller
  rescue_from Cybertrain::RecordNotFound, with: :record_not_found

  private

  def record_not_found
    render plain: "Not Found", status: :not_found
  end
end

Вы здесь:Вы пишете app/, db/migrate/

Скаффолд статьи

ЗачемОдна команда создаёт весь CRUD-срез, как в Rails.

терминал — blog/shell
cybertrain generate scaffold article title:string body:text

Она создаёт миграцию, модель, контроллер и пять представлений (index, show, new, edit, _form) и вставляет resources :articles в config/routes.rb. Типы полей: string, text, integer, float, boolean, date, datetime и references; по умолчанию — string. cybertrain g scaffold из быстрого старта — та же самая команда.

Существующие файлы никогда не перезаписываются: для каждого файла выводится create, identical или exist.

Вы здесь:Вы пишете config/routes.rb

Корневой маршрут

Зачем/ должен показывать список статей.

config/routes.rbruby
# config/routes.rb
Cybertrain::Routes.draw do
  root "articles#index"
  resources :articles
end

Маршруты читаются при сборке: spin run gen превращает их в gen/routes.rb. cybertrain server, cybertrain db migrate и cybertrain build запускают его за вас.

Вы здесь:Вы пишете app/models/

Валидации модели

ЗачемКак и в руководстве Rails, добавьте валидацию длины вручную.

app/models/article.rbruby
# app/models/article.rb -- only the first string field gets a default
# presence validation; add anything past that yourself.
class Article
  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

У class Article суперкласс не указан: суперкласс Cybertrain::Model, столбцы и finder-методы берутся из gen/models/article.rb. Неудачное сохранение выдаёт сообщения вроде «Title can't be blank» и «Body is too short (minimum is 10 characters)».

Вы здесь:Вы пишете app/controllers/

ArticlesController

ЗачемПолный CRUD в духе Rails, вплоть до strong params и связки «flash, затем редирект», — за исключением двух вещей, которые Spinel навязывает везде.

app/controllers/articles_controller.rbruby
# app/controllers/articles_controller.rb (index/show/edit are as plain as
# Rails' own scaffold; update mirrors create)
class ArticlesController < ApplicationController
  before_action :set_article, only: [:show, :edit, :update, :destroy]

  def new_action   # a `new` method would shadow ArticlesController.new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)
    if @article.save
      flash[:notice] = "Article was successfully created."
      redirect_to article_path(@article), status: :see_other
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def set_article
    @article = Article.find(params[:id])
  end

  def article_params
    params.require(:article).permit(:title, :body)
  end
end

Скаффолд создаёт полный контроллер, включая также index, show, edit, update и destroy; во фрагменте выше они опущены. В index — @articles = Article.all.to_a: .to_a обязателен.

Переменные экземпляра находятся лексическим сканированием @ivar =, поэтому после добавления новой заново запустите cybertrain spin run gen; сервер разработки делает это за вас.

Вы здесь:Сборка, затем запуск gen/ → build/bin/blog

Генерация, миграции, запуск

ЗачемПревратить схему и маршруты в gen/, создать базу данных SQLite, запустить сервер.

терминал — blog/shell
cybertrain db migrate    # spin run gen; spin run db -- migrate; spin run gen
cybertrain server        # http://127.0.0.1:3000; пересборка при правках Ruby, представления перезагружаются без пересборки

spin run gen читает db/schema.rb и config/routes.rb, сканирует app/ и записывает gen/; spin run db -- migrate применяет db/migrate/*.rb и перезаписывает db/schema.rb. Порядок важен: первый spin run gen помещает новую миграцию в gen/migrations.rb, который подключает bin/db.rb, а второй строит модели по перезаписанному db/schema.rb.

cybertrain server выводит каждую запускаемую команду spin, а затем стартовый баннер приложения. При первом запуске приложение компилируется, так что это займёт некоторое время:

вывод — cybertrain servertext
=> Booting cybertrain 0.2.1
=> development environment (1 worker)
=> Watching app/, config/ and db/schema.rb for changes
* Listening on http://127.0.0.1:3000
Use Ctrl-C to stop
  • Заново запускайте cybertrain spin run gen и коммитьте gen/ после изменения схемы, маршрутов или колбэков и переменных экземпляра контроллера либо модели. Сервер разработки делает это за вас.
  • Порт меняется так: cybertrain server 4000 (или PORT=4000).
  • cybertrain db добавляет --, который нужен spin run db перед аргументами; cybertrain db status и другие команды базы данных работают так же.

Вы здесь:Вы пишете db/migrate/, app/

Скаффолд комментариев

ЗачемТип references даёт внешний ключ, из которого выводится ассоциация.

терминал — blog/shell
cybertrain generate scaffold comment commenter:string body:text article:references
cybertrain db migrate    # скаффолд создал миграцию для таблицы comments

article:references добавляет столбец article_id, индекс и внешний ключ — этого достаточно, чтобы Article/Comment получили ассоциацию; писать has_many/belongs_to не нужно, spin run gen выводит её из внешнего ключа. Получившаяся схема:

db/schema.rb (фрагмент)ruby
  s.create_table "comments" do |t|
    t.string "commenter"
    t.text "body"
    t.integer "article_id", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  s.add_index "comments", ["article_id"], name: "index_comments_on_article_id"

  s.add_foreign_key "comments", "articles", column: "article_id"

Скаффолд вставляет отдельную строку resources :comments, которую заменит следующий шаг.

Вы здесь:Вы пишете config/routes.rb

Вложенные маршруты

ЗачемВ руководстве Rails комментарии вложены в статьи.

config/routes.rbruby
Cybertrain::Routes.draw do
  root "articles#index"

  # A nested block takes the mapper explicitly (Spinel's instance_eval
  # trampoline only rewires `self` for the outermost block): `articles.resources`.
  resources :articles do |articles|
    articles.resources :comments, only: [:create, :destroy]
  end
end

Вы здесь:Вы пишете app/controllers/

CommentsController

ЗачемДоступны только два действия, а родительская статья берётся из URL (params[:article_id]), а не из :id.

app/controllers/comments_controller.rbruby
class CommentsController < ApplicationController
  before_action :set_article

  def create
    @comment = Comment.new(comment_params)
    @comment.article_id = @article.id
    if @comment.save
      flash[:notice] = "Comment was successfully created."
    else
      flash[:alert] = "Comment could not be saved: #{@comment.errors.full_messages.join(", ")}"
    end
    redirect_to article_path(@article), status: :see_other
  end

  def destroy
    @comment = Comment.where(article_id: @article.id).find(params[:id])
    @comment.destroy
    flash[:notice] = "Comment was successfully destroyed."
    redirect_to article_path(@article), status: :see_other
  end

  private

  def set_article
    @article = Article.find(params[:article_id])
  end

  def comment_params
    params.require(:comment).permit(:commenter, :body)
  end
end

Затем добавьте валидации в модель комментария:

app/models/comment.rbruby
class Comment
  validates :commenter, presence: true
  validates :body, presence: true
end

Article#comments здесь — это Array, поэтому рельсовый @article.comments.find(params[:id]) превращается в Comment.where(article_id: @article.id).find(params[:id]). Комментарий под чужой статьёй возвращает 404.

Вы здесь:Запуск app/views/, при разработке читаются с диска, встраиваются командой cybertrain build

Представления и партиалы

ЗачемСтраница статьи выводит список её комментариев и заканчивается формой, привязанной к [@article, @comment].

Сначала удалите ставшие недоступными представления комментариев index, show, new и edit и добавьте в ArticlesController#show пустой @comment:

app/controllers/articles_controller.rb (фрагмент)ruby
  def show
    @comment = Comment.new
  end

Затем выведите комментарии и форму на странице статьи:

app/views/articles/show.html.erb · app/views/comments/_form.html.erberb
<%# app/views/articles/show.html.erb %>
<% @article.comments.each do |comment| %>
  <%= render "comments/comment", comment: comment %>
<% end %>
<%= render "comments/form" %>

<%# app/views/comments/_form.html.erb %>
<%= form_with(model: [@article, @comment]) do |f| %>
  <%= f.label :commenter %> <%= f.text_field :commenter %>
  <%= f.label :body %> <%= f.text_area :body %>
  <%= f.submit %>
<% end %>

Партиал комментария в том виде, в каком он есть в примере:

app/views/comments/_comment.html.erberb
<%# locals: (comment:) %>
<div class="comment">
  <p>
    <strong>Commenter:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>Comment:</strong>
    <%= comment.body %>
  </p>

  <%= button_to "Destroy comment", [@article, comment], method: :delete %>
</div>

И button_to ... [@article, comment], и form_with(model: [@article, @comment]) разрешаются по одному и тому же правилу [parent, child]: новый дочерний объект направляется во вложенную коллекцию (article_comments_path), сохранённый — во вложенный элемент (article_comment_path); эти хелперы генерируются в gen/routes.rb из вложенного блока resources выше.

Вы здесь:Сборка cybertrain spin test

Тестирование

Зачемminitest и RSpec находят тесты через рефлексию, на которую AOT-компилятор не способен, поэтому CyberTrain поставляется с Cybertrain::Test.

Тест модели

test/articles.rb (отрывок)ruby
require_relative "support/blog_test"   # the app, BLOG and Cybertrain::Test

test "title must be present" do
  article = Article.new(body: "a body long enough to pass length")
  refute article.save
  assert_includes article.errors.full_messages, "Title can't be blank"
end

Утверждения: assert, refute, assert_equal, assert_nil, assert_includes, assert_raises("Class") { }, flunk. В примере также используются assert_response и assert_redirected_to.

Полный HTTP-сценарий

test/articles.rb (отрывок)ruby
require_relative "support/blog_test"   # the app, BLOG and Cybertrain::Test

test "POST /articles creates an article and redirects to it" do
  client = Cybertrain::Test::Client.new(BLOG)
  client.get("/articles/new")
  res = client.post("/articles", { "authenticity_token" => BlogTest.form_token(client),
                                   "article[title]" => "Hello Rails",
                                   "article[body]" => "I am on Rails! This is my first article." })
  assert_redirected_to res, BlogTest.article_path(Article.last)
  assert_response res, :see_other
end

Cybertrain::Test::Client прогоняет приложение внутри процесса (без сокета) с хранилищем cookie, поэтому сессии и CSRF ведут себя так же, как за браузером. Запросы без токена получают 403.

Запуск тестов

терминал — blog/shell
cybertrain spin test

cybertrain spin test запускает spin test с зафиксированным Spinel; если инструменты есть в PATH (шаг 00), то же самое делает spin test. Он компилирует каждый test/*.rb в отдельную программу и сравнивает её вывод (stdout и stderr вместе) с закоммиченным снимком test/<name>.rb.expected. Файлы в test/support/ подключаются, а не запускаются. Чтобы записать снимок:

терминал — blog/shell
cybertrain spin test --regen test/<name>.rb
rm -f build/test/<name>; cybertrain spin test test/<name>.rb; [ -x build/test/<name> ] && ./build/test/<name> > test/<name>.rb.expected 2>&1

--regen записывает снимок по выводу CRuby. Тесты, обращающиеся к SQLite через FFI, не могут работать под CRuby, поэтому их снимок берётся из скомпилированного бинарного файла (вторая строка выше). Пока снимок устарел, cybertrain spin test завершается с ненулевым кодом, поэтому после него стоит ;, а не &&; проверка [ -x ] пропускает запись, если компиляция не удалась, и старый снимок сохраняется. Тестам, которые не могут работать под CRuby, этот снимок нужен, прежде чем cybertrain spin test сможет пройти; остальные тесты без снимка сравниваются с выводом CRuby.

Вы здесь:Запуск dist/blog

Сборка для развёртывания

ЗачемОдна команда собирает приложение в готовый к развёртыванию каталог dist/ со всеми встроенными представлениями.

терминал — blog/shell
cybertrain build

Она записывает dist/: бинарный файл приложения со всеми встроенными представлениями и рядом с ним файлы, которые он использует. Она запускает spin run gen -- --embed-views, spin build blog и spin run gen (который возвращает закоммиченный пустой gen/views.rb, даже после сбоя или Ctrl-C), а затем собирает:

  • dist/blogскомпилированные фреймворк, контроллеры, модели и интерпретатор шаблонов со всеми встроенными представлениями
  • dist/public/статические ресурсы и страницы ошибок; можно отдавать их через обратный прокси
  • dist/storage/создаётся пустым; по умолчанию здесь файл SQLite (пересборка никогда его не очищает)
  • dist/tmp/создаётся пустым (пересборка никогда его не очищает)

Скопируйте dist/ на машину с той же ОС и тем же процессором (бинарный файл компонуется с системной SQLite) и запустите там. Собранный бинарный файл по умолчанию работает в окружении production (CYBERTRAIN_ENV переопределяет это). Задайте CYBERTRAIN_SECRET_KEY_BASE; для примера:

терминал — продакшен-серверshell
cd dist
export CYBERTRAIN_SECRET_KEY_BASE="$(openssl rand -hex 32)"
./blog migrate && ./blog

./blog migrate применяет миграции, вкомпилированные в бинарный файл, а ./blog db status показывает их состояние; PORT задаёт порт. Не меняйте секрет между перезапусками: его смена завершает все сессии.

Cybertrain::Config сначала читает переменные окружения; config/app.rb их переопределяет:

ПеременнаяАтрибутПо умолчанию
CYBERTRAIN_ENVenv"development"; "production" в бинарном файле из cybertrain build (также "test")
PORTport3000
CYBERTRAIN_DATABASEdatabase_pathstorage/#{env}.sqlite3
CYBERTRAIN_SECRET_KEY_BASEsecret_key_baseобязательна в production; в dev/test генерируется автоматически в tmp/secret_key
SPINEL_WORKERSworkers1
CYBERTRAIN_HOSThost"127.0.0.1"; 0.0.0.0 слушает на всех интерфейсах (контейнеры)
CYBERTRAIN_SESSION_SAME_SITEsession_same_site"Lax"; также Strict или None (None всегда добавляет Secure); любое другое значение останавливает сервер при запуске
CYBERTRAIN_SESSION_PARTITIONEDsession_partitionedfalse; 1 или true добавляет Partitioned (и Secure)
config/app.rbruby
# config/app.rb
Cybertrain.configure do |c|
  c.port = 3000
  c.workers = 1
end
  • Сервер говорит на обычном HTTP/1.1: для TLS и HTTP/2 поставьте перед ним nginx или Caddy. В production cookie сессии имеет флаг Secure, поэтому отдавайте приложение по HTTPS (задавайте c.session_secure = false в config/app.rb, только если production действительно работает по обычному http).
  • Без CYBERTRAIN_SECRET_KEY_BASE production отказывается запускаться.
  • В production исключение даёт ответ 500, а голый ответ с ошибкой (текстовый 404 маршрутизатора, head :not_found, render plain:) заменяется на public/<status>.html, если такой файл существует; cybertrain new создаёт 404.html и 500.html.
  • SIGTERM прекращает приём соединений, даёт выполняющимся запросам завершиться и выходит, когда они закончатся, или по истечении drain_timeout сервера (10 с).
  • Слежение за файлами и цикл пересборки никогда не работают в production. Запущенный cybertrain server пропускает пересборки, пока cybertrain build удерживает tmp/cybertrain-build.lock.

Что нужно знать

ЗачемОграничения и особенности, о которых лучше знать заранее, чтобы они не застали вас врасплох.

  • Сессии хранят только строки (String) в cookie с подписью HMAC.
  • Только SQLite, через FFI Spinel.
  • Бэктрейсов нет: ошибки показывают класс, сообщение, строку запроса и имя/строку шаблона.
  • Атрибут с опечаткой в представлении выводится пустым вместо исключения; это известный пробел.
  • Ассоциации выводятся только из внешних ключей схемы; нет has_many :through, includes, pluck, dependent:, enum, STI и полиморфных ассоциаций.
  • Нет namespace, respond_to/форматов, constraints и mount; render json: минимален.
  • В модели, созданной скаффолдом, валидацию присутствия по умолчанию получает только первое строковое поле.
  • Генератор обрабатывает app/controllers и app/models лексическим сканированием (регулярными выражениями), а не парсером.
  • Представления встраиваются при сборке: даже изменение только в представлении требует cybertrain build и перезапуска, прежде чем оно попадёт в production.
  • Собирайте dist/ на той же ОС и том же процессоре, что и у машины, где он будет работать: бинарный файл компонуется с системной SQLite.
  • SPINEL_WORKERS по умолчанию равен 1.
  • Протестирован только Spinel 2026.09.12.

Отличия от Rails

ЗачемПолная таблица из README в одном месте.

Различия между Rails и CyberTrain (полная таблица из README)
RailsCyberTrain
rails consoleКонсоли нет — в Spinel нет eval
Правите код — запущенное приложение подхватывает измененияRuby требует пересборки; в режиме разработки cybertrain server делает это за вас (пересборка, остановка, execv)
Правите представление — перезагрузка не нужнаТак же — в режиме разработки представления разбираются с диска при каждом запросе
def newdef new_action (new затенил бы Klass.new(ctx))
before_action { do_thing } (неявный self)before_action { |c| c.do_thing } — для сохранённого блока нет instance_exec, поэтому колбэки получают контроллер/запись явно
resources :posts do resources :comments endresources :posts do |posts| posts.resources :comments end — вложенные блоки получают маппер явно
ERB компилируется в метод; любой объект, любой методИнтерпретируется по фиксированной грамматике/таблице диспетчеризации
Сессия хранит любой сериализуемый через Marshal объектЗначения сессии — только строки (String), cookie с подписью HMAC
Множество адаптеров баз данныхТолько SQLite, через FFI
has_many :through, includes, pluck, dependent:, enum, STI, полиморфные ассоциацииНе реализовано; ассоциации выводятся только из внешних ключей схемы
namespace, форматы/respond_to, constraints, mountНе реализовано — плоские имена, только render json:
Полный бэктрейс при исключенииКласс, сообщение, строка запроса, имя/строка шаблона — Spinel не предоставляет бэктрейсов
Rack, его middleware и любой гем в GemfileНет совместимости с Rack; небольшой фиксированный набор middleware; собственный spin-index Spinel, ограниченный тем, что компилируется в рамках его подмножества Ruby
minitest / RSpecCybertrain::Test — раннеры на основе рефлексии не могут работать при AOT-компиляции
Аутентификация, мейлеры, фоновые задачи, ActionCable, asset pipeline, i18nНе сделано (за рамками MVP)

Список не исчерпывающий.

Что дальше