第 5 章: パッケージ管理と静的解析¶
5.1 はじめに¶
前の章では、何をコミットし、何をコミットしないかを決めました。この章では、コミットした情報から 同じ環境を作り直す 仕組み(パッケージ管理)と、コードの品質を 機械的に確かめる 仕組み(静的解析・カバレッジ)を整えます。
Ruby の道具立ては、Python 版とよく似た「役割ごとに別の道具を足す」構成です。
| 役割 | Ruby | Python 版 | Rust 版 |
|---|---|---|---|
| 依存の解決 | Bundler(rubygems.org) | uv(PyPI) | cargo(crates.io) |
| 依存の記録 | Gemfile・Gemfile.lock |
pyproject.toml・uv.lock |
Cargo.toml・Cargo.lock |
| 整形 | RuboCop(Layout/*) |
Ruff(ruff format) |
rustfmt |
| 型検査 | なし(テストで捕まえる) | mypy | rustc |
| 静的解析 | RuboCop(Style/*・Lint/*・Metrics/*) |
Ruff(ruff check) |
clippy |
| テスト | Minitest | pytest | cargo test |
| カバレッジ | SimpleCov | pytest-cov | cargo-llvm-cov |
| 設定ファイル | Gemfile・.rubocop.yml・test/test_helper.rb |
pyproject.toml |
Cargo.toml |
大きな違いは 2 つです。型検査の道具を使わないこと(Ruby 版は RBS・Steep を使わないと決めています)と、整形と静的解析が RuboCop 1 つに入っていることです。Python 版の Ruff も両方を持っていますが、ruff format と ruff check の 2 つのコマンドに分かれていました。RuboCop は 1 回の実行で、字下げも書き方もまとめて検査します。
5.2 Bundler によるパッケージ管理¶
Gemfile¶
Ruby のプロジェクトの依存は Gemfile に書きます。第 3 章までの apps/ruby/Gemfile はこれだけです。
# frozen_string_literal: true
source "https://rubygems.org"
ruby ">= 3.3"
gem "csv", "~> 3.3"
gem "rumale", "~> 2.2"
group :development, :test do
gem "minitest", "~> 6.0"
gem "rake", "~> 13.0"
gem "rubocop", "~> 1.91", require: false
gem "simplecov", "~> 1.3", require: false
end
| 項目 | 意味 |
|---|---|
source |
gem を取ってくる場所 |
ruby ">= 3.3" |
動かせる Ruby の版(5.3 節) |
gem "rumale", "~> 2.2" |
依存する gem と、受け入れる版の範囲 |
group :development, :test |
開発とテストでだけ使う gem |
require: false |
Bundler.require で自動的に読み込まない。RuboCop と SimpleCov は、使う場所で自分で require する |
Gemfile は Ruby のコードそのものです。pyproject.toml(TOML)や Cargo.toml のような設定ファイルの形式ではなく、gem や group というメソッドを呼ぶ DSL になっています。先頭の # frozen_string_literal: true は、Gemfile も RuboCop の検査の対象になっているからです。
csv を明示しているのは、Ruby 3.4 で csv が default gem から bundled gem に移ったからです。Gemfile に書いておけば、Ruby の版を上げても同じ版の csv を使い続けられます。
悲観的バージョン制約 ~>¶
"~> 2.2" は「2.2 を使う」という意味ではありません。最後の桁だけを上げてよい という意味の制約です。英語では pessimistic version constraint(悲観的バージョン制約)と呼ばれます。
| 書き方 | 意味する範囲 | 受け入れる例 |
|---|---|---|
"~> 2.2" |
>= 2.2, < 3.0 |
2.2.0、2.9.1 |
"~> 2.2.0" |
>= 2.2.0, < 2.3 |
2.2.0、2.2.5 |
"~> 1.91" |
>= 1.91, < 2.0 |
1.91.0、1.99.0 |
"= 2.2.0" |
2.2.0 だけ | 2.2.0 |
">= 3.3" |
3.3 以上すべて | 3.3.10、4.0.0 |
Rust 版のキャレット要件("0.8" は >=0.8.0, <0.9.0)と違い、~> は 書いた桁数で範囲が決まります。"~> 2.2" と "~> 2.2.0" は受け入れる範囲が違うので、どこまで上がってよいかを桁数で表します。Rumale 自身も、分割した gem どうしを rumale-core (~> 2.2.0) のように 3 桁で縛り、同じ 2.2 系の中だけで組み合わさるようにしています。
Gemfile.lock¶
解決の結果は Gemfile.lock に書かれます。第 3 章までで、Gemfile に書いた 6 個から、推移的依存を含めて 42 個の gem が並びます。
rumale (2.2.0)
numo-narray-alt (>= 0.9.10, < 0.12.0)
rumale-clustering (~> 2.2.0)
rumale-core (~> 2.2.0)
rumale-decomposition (~> 2.2.0)
lock の末尾には、Gemfile に書いた依存・Ruby の版・Bundler の版が記録されます。
DEPENDENCIES
csv (~> 3.3)
minitest (~> 6.0)
rake (~> 13.0)
rubocop (~> 1.91)
rumale (~> 2.2)
simplecov (~> 1.3)
RUBY VERSION
ruby 3.3.10p183
BUNDLED WITH
2.7.2
Rust 版の Cargo.lock には各クレートのチェックサムがありましたが、本シリーズの Gemfile.lock には チェックサムの節(CHECKSUMS)がありません。Bundler 2.7.2 には bundle lock --add-checksums という選択肢があり、使えば lock にチェックサムを記録できます。本シリーズではまだ使っていません。
lock ファイルがあると、bundle install は 記録された版をそのまま使います。
bundle install
Bundle complete! 6 Gemfile dependencies, 43 gems now installed.
Bundled gems are installed into `./vendor/bundle`
lock の 42 個に Bundler 自身を加えた 43 個が、第 4 章で見た vendor/bundle に入ります。
新しい版があるかは bundle outdated で確かめます。--only-explicit を付けると、Gemfile に書いた gem だけを調べます。
bundle outdated --only-explicit
Fetching gem metadata from https://rubygems.org/...........
Resolving dependencies...
Bundle up to date!
| コマンド | 何をするか |
|---|---|
bundle add <gem> |
Gemfile に gem を足し、lock を更新する |
bundle install |
lock のとおりに gem を入れる。lock が無ければその場で作る |
bundle outdated |
新しい版がある gem を表示する |
bundle update <gem> |
Gemfile の範囲内で、指定した gem の lock を上げる |
bundle lock --add-platform <platform> |
lock にプラットフォームを足す(第 4 章) |
bundle exec <command> |
lock の版の gem で command を実行する |
bundle update を gem の名前なしで実行すると、すべての gem を一度に上げます。上げるときは 1 つずつ名前を指定し、テストを通してからコミットするほうが、何が結果を変えたのかを追いやすくなります。
本番依存と開発依存¶
Gemfile の group が、Python 版の [dependency-groups]、Rust 版の [dev-dependencies] に当たります。
group :development, :test do
gem "minitest", "~> 6.0"
# ...
end
本シリーズでは、csv と rumale だけが章のプログラムの実行に必要で、Minitest・Rake・RuboCop・SimpleCov は開発とテストでだけ使います。本番の環境で開発用の gem を入れないときは、BUNDLE_WITHOUT="development:test" bundle install のように除くグループを指定します。
5.3 Ruby の版を固定する¶
Ruby の版は、3 つの場所に書かれています。
| 場所 | 書いてあること | 誰が読むか |
|---|---|---|
Gemfile の ruby ">= 3.3" |
動かせる版の下限 | Bundler(合わなければ bundle install が止まる) |
Gemfile.lock の RUBY VERSION |
lock を作ったときの版(3.3.10p183) | 記録 |
.ruby-version の 3.3 |
使う版 | rbenv・chruby などのバージョン管理ツール |
実際に Ruby を用意しているのは Nix の環境定義です。
buildInputs = baseShell.buildInputs ++ (with packages; [
ruby
rubyPackages_3_3.solargraph
bundler
]);
nix develop .#ruby
Ruby development environment activated
- Ruby: ruby 3.3.10 (2025-10-23 revision 343ea05002) [x86_64-darwin24]
- Bundler: Bundler version 2.7.2
- Solargraph: 0.57.0
第 4 章で見たとおり、Random と Array#shuffle の並びは Ruby 本体が決めます。Rust 版の rand クレートの版を Cargo.lock で固定したのと同じ役目を、Ruby 版では Nix の環境定義 が担っています。
macOS に付属する Ruby は 2.6 です。Data.define(Ruby 3.2 以降)などを使う本シリーズのコードは動きません。付属の Ruby で版を確かめると、条件を満たさないことが終了コードで分かります。
/usr/bin/ruby -e 'exit(Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.3"))'; echo $?
1
この 1 行は、第 6 章の Gulp のタスクが「手元の Ruby を使うか、Nix の Ruby を使うか」を決めるときにも使います。
5.4 numo-narray から numo-narray-alt へ¶
Ruby 版で依存を選ぶときに 1 か所、気をつけたところがあります。Rumale が使う数値計算の gem です。
Ruby の数値計算では、長く numo-narray(Python の NumPy に当たる多次元配列)が使われてきました。ところが本家の numo-narray は 2022 年のリリースを最後に更新が止まっています。Rumale の作者は、その fork である numo-narray-alt を保守していて、Rumale 2.x はこちらに依存します。
numo-narray-alt (0.11.2)
numo-optimize (0.4.0)
numo-narray-alt (>= 0.9.9, < 0.12.0)
名前が -alt でも、Ruby のコードからは同じ Numo::NArray・Numo::DFloat として使います。第 3 章で Numo::DFloat.cast(...) と書いたのは、numo-narray-alt が定義したクラスです。
| 観点 | numo-narray(本家) | numo-narray-alt |
|---|---|---|
| 保守 | 2022 年で止まっている | Rumale の作者が保守している |
| Rumale 2.x | 依存しない | 依存する(>= 0.9.10, < 0.12.0) |
| Ruby のコードからの名前 | Numo::NArray |
Numo::NArray(同じ) |
| ネイティブ拡張 | あり | あり(bundle install でビルドする) |
numo-narray-alt の README は、モジュール名・クラス名・メソッド名を変えない「drop-in replacement」だと説明しています。require "numo/narray" で読み込め、numo-narray-alt であることを明示したいときは require "numo/narray/alt" と書きます。
「インターネットで見つかる numo-narray の記事」と「いま Rumale と一緒に入る gem」が違う、ということを覚えておいてください。Gemfile に numo-narray を自分で足すと、同じ require の名前と同じクラス名を持つ gem が 2 つ並び、どちらが読み込まれるのかが分かりにくくなります。本シリーズでは numo 系の gem を Gemfile に直接書かず、Rumale が選んだものに任せています。gem の選定の経緯は ADR 010 を参照してください。
numo-narray-alt は C で書かれたネイティブ拡張(gem の中の ext/)を持つので、bundle install のときに手元でコンパイルします。
5.5 静的解析 — RuboCop¶
RuboCop の設定¶
RuboCop の設定は apps/ruby/.rubocop.yml です。第 1 章で作り、この章で 1 つ足しました。
AllCops:
TargetRubyVersion: 3.3
NewCops: enable
SuggestExtensions: false
Exclude:
- "vendor/**/*"
# テストのメソッド名に日本語を使うので、ASCII 以外の識別子を許す。
Naming/AsciiIdentifiers:
Enabled: false
Style/StringLiterals:
EnforcedStyle: double_quotes
# テストのメソッド名は日本語で振る舞いを書くので、snake_case を求めない。
Naming/MethodName:
Exclude:
- "test/**/*"
# 機械学習の慣例(特徴量 x・正解ラベル t・y)の 1 文字の名前を許す。
Naming/MethodParameterName:
AllowedNames: [x, t, y]
# テストは期待する出力をヒアドキュメントでそのまま書くので、メソッドの行数を求めない。
Metrics/MethodLength:
Exclude:
- "test/**/*"
# テストは 1 つの振る舞いを 1 つのメソッドに書くので、章のテストのクラスは長くなる。
Metrics/ClassLength:
Exclude:
- "test/**/*"
RuboCop のルールは cop(警官)と呼ばれ、部門(department)に分かれています。
| 部門 | 内容 | 本シリーズでの例 |
|---|---|---|
Layout |
字下げ・空白・改行などの見た目 | Layout/EndOfLine(第 4 章) |
Style |
慣用的な書き方 | Style/StringLiterals(二重引用符にそろえる) |
Lint |
誤りの可能性が高い書き方 | Lint/UselessAssignment(使っていない変数) |
Metrics |
メソッドやクラスの長さ・複雑さ | Metrics/MethodLength・Metrics/ClassLength |
Naming |
名前の付け方 | Naming/MethodName・Naming/AsciiIdentifiers |
Rust 版の clippy がグループ単位で「使う・使わない」を決めたのに対して、RuboCop は 既定でほぼすべての cop が有効 で、合わないものを 1 つずつ外していきます。外した cop にはすべて、なぜ外すのかをコメントで書いています。
NewCops を明示しないとどうなるか¶
NewCops: enable は、RuboCop の版を上げたときに増えた新しい cop(pending の cop)を有効にする指定です。.rubocop.yml から NewCops: enable の行だけを消した設定を tmp/nonew.yml に作り、それで実行してみました。
bundle exec rubocop -c tmp/nonew.yml lib/getting_started_ml/dataset.rb
The following cops were added to RuboCop, but are not configured. Please set Enabled to either `true` or `false` in your `.rubocop.yml` file.
Please also note that you can opt-in to new cops by default by adding this to your config:
AllCops:
NewCops: enable
Gemspec/AddRuntimeDependency: # new in 1.65
Enabled: true
Gemspec/AttributeAssignment: # new in 1.77
Enabled: true
# new in の行を数えると 163 個ありました。1 ファイルを検査するだけで、163 個の cop について「有効にするか決めてください」と言われます。これでは本当の指摘が埋もれるので、本シリーズは NewCops: enable で新しい cop をまとめて有効にしています。代わりに、RuboCop の版を上げると新しい cop が増え、それまで通っていたコードが指摘されることがあります。Gemfile.lock で RuboCop の版を固定している(~> 1.91)ので、それが起きるのは自分で bundle update rubocop したときだけ、のはずです。ただし Nix の環境には、この前提を崩す落とし穴がありました(第 6 章の 6.6 節)。
わざと違反を入れて、検査が効いていることを確かめる¶
検査は「動いているつもり」が一番危ないので、違反を入れて失敗することを確かめます。Rust 版の 5.6 節と同じ種類の違反を、Ruby で書きました。
# frozen_string_literal: true
module GettingStartedMl
# 静的解析の確認用。記事のために違反を並べたモジュール。
module Zzdemo
module_function
# 種類が setosa かどうかを返す。
def setosa?(name)
if name == "setosa" then true else false end
end
# 値を順に表示する。
def show(values)
for i in 0...values.size
puts values[i]
end
end
# 欠損値があるかどうかを返す。
def missing?(value)
value == nil
end
# 合計を求める。
def total(values)
sum = 0.0
count = values.size
values.each { |value| sum += value }
sum
end
end
end
bundle exec rake check
Running RuboCop...
RuboCop failed!
Inspecting 21 files
...........CW.C......
Offenses:
lib/getting_started_ml/zzdemo.rb:10:7: C: [Correctable] Style/IfWithBooleanLiteralBranches: Remove redundant if with boolean literal branches.
if name == "setosa" then true else false end
^^
lib/getting_started_ml/zzdemo.rb:10:7: C: [Correctable] Style/OneLineConditional: Favor the ternary operator (?:) or multi-line constructs over single-line if/then/else/end constructs.
if name == "setosa" then true else false end
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
lib/getting_started_ml/zzdemo.rb:10:7: C: [Correctable] Style/RedundantConditional: This conditional expression can just be replaced by name == "setosa".
if name == "setosa" then true else false end
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
lib/getting_started_ml/zzdemo.rb:15:7: C: [Correctable] Style/For: Prefer each over for.
for i in 0...values.size ...
^^^^^^^^^^^^^^^^^^^^^^^^
lib/getting_started_ml/zzdemo.rb:22:13: C: [Correctable] Style/NilComparison: Prefer the use of the nil? predicate.
value == nil
^^
lib/getting_started_ml/zzdemo.rb:28:7: W: [Correctable] Lint/UselessAssignment: Useless assignment to variable - count.
count = values.size
^^^^^
test/chapter02_test.rb:5:1: C: Metrics/ClassLength: Class has too many lines. [101/100]
class Chapter02Test < Minitest::Test ...
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(同時に置いた CRLF のファイルへの Layout/EndOfLine の指摘は、第 4 章で見たので省いています)
違反を入れた 4 つのメソッドから、6 件の指摘が出ました。左端の 1 文字は重さで、C は Convention(慣習)、W は Warning(警告)です。
| 指摘 | 部門 | 伝えていること | Rust 版の対応する指摘 |
|---|---|---|---|
IfWithBooleanLiteralBranches・RedundantConditional |
Style | if ... then true else false end は条件式そのものでよい |
needless_bool |
OneLineConditional |
Style | 1 行の if/then/else/end より三項演算子か複数行 |
なし |
Style/For |
Style | for より each |
needless_range_loop |
Style/NilComparison |
Style | == nil より nil? |
partialeq_to_none |
Lint/UselessAssignment |
Lint | 使っていない変数 | rustc の unused_variables |
1 つの書き方に 3 つの cop が反応しているのが RuboCop らしいところです。Style/For は、Ruby では for がブロックを作らず、ループの変数がループの外に漏れることを嫌う cop です。Rust 版の &Vec<f64> への指摘(ptr_arg)に当たるものはありません。Ruby には借用や型の区別が無いからです。
[Correctable] の付いた指摘は、bundle exec rubocop -a(安全な修正だけ)で自動的に直せます。
最後の 1 件は、この章で足したテストから出た¶
指摘の最後の 1 件は、違反を入れたファイルからではありません。第 4 章で並べ替えの並びを固定するテストを足したら、Chapter02Test が 101 行になり、Metrics/ClassLength の上限 100 行を超えたのです。
直し方は 2 つ考えられます。テストのクラスを分けるか、test/ では Metrics/ClassLength を外すかです。本シリーズでは後者にしました。章のテストは「1 つの振る舞いを 1 つのメソッド」に書くので、章が進むと自然に長くなります。長さを抑えるためにテストをまとめると、どの振る舞いが壊れたのかが分かりにくくなります。Metrics/MethodLength を test/ で外したのと同じ理由です。
RuboCop の既定の設定(config/default.yml)には、Metrics/ClassLength について次のコメントがあります。
Metrics/ClassLength:
Description: 'Avoid classes longer than 100 lines of code.'
Enabled: true
# Expected to be disabled by default in the next major release.
# Rejected by a third of projects; those that keep it set the limit three times higher.
「次のメジャーリリースでは既定で無効になる見込み。3 分の 1 のプロジェクトが拒否し、残すプロジェクトは上限を 3 倍にしている」。RuboCop 自身が、この cop は意見が分かれると認めています。外すときは、lib/ には効かせたまま test/ だけを外し、理由を .rubocop.yml のコメントに残しました。
抑える範囲¶
指摘を抑える書き方は 3 つあり、範囲が違います。
| 書き方 | 範囲 |
|---|---|
行末の # rubocop:disable Style/For |
その行(# rubocop:disable と # rubocop:enable で挟めばその範囲) |
.rubocop.yml の Exclude |
指定したファイル |
.rubocop.yml の Enabled: false |
プロジェクト全体 |
いちばん狭い範囲で抑えるのが原則です。本シリーズは今のところコードの中に # rubocop:disable を 1 つも書いていません。抑えているのは、テストのメソッド名を日本語にするための Naming の 2 つと、テストの長さの Metrics の 2 つで、どれも test/ に限っています(Naming/AsciiIdentifiers だけはプロジェクト全体で外しています)。Rust 版と同じく、何を抑えるかより、なぜ抑えるか を書き残すほうが大切です。
5.6 コードカバレッジ — SimpleCov¶
Ruby の標準ライブラリには、行ごとの実行回数を数える Coverage があります。SimpleCov はそれを使ってテストの実行を計測し、報告を作る gem です。第 1 章で test/test_helper.rb に組み込みました。
require "simplecov"
SimpleCov.start do
skip "/test/"
skip "/vendor/"
end
bundle exec rake test を実行すると、最後に行カバレッジが表示され、coverage/index.html に HTML の報告が出ます。第 3 章までで、学習データがある状態では次のとおりでした。
50 runs, 85 assertions, 0 failures, 0 errors, 0 skips
Coverage report generated for Minitest to coverage/index.html
Line coverage: 207 / 220 (94.09%)
Rust 版の cargo-llvm-cov がリージョン・関数・行の 3 つを出したのに対して、SimpleCov の既定は 行だけ です(分岐のカバレッジは enable_coverage :branch で有効にできますが、本シリーズでは使っていません)。
数字の読み方に注意する¶
上の数字は、学習データがある状態 で測ったものです。第 4 章で見たとおり、実データのテストはデータが無ければスキップします。スキップしたテストが通るはずだった行は計測されないので、カバレッジが下がります。
ML_DATA_DIR=/nonexistent bundle exec rake test
50 runs, 68 assertions, 0 failures, 0 errors, 7 skips
You have skipped tests. Run with --verbose for details.
Coverage report generated for Minitest to coverage/index.html
Line coverage: 184 / 220 (83.63%)
| 測り方 | テスト | 行カバレッジ |
|---|---|---|
| データあり | 50 runs・0 skips | 207 / 220(94.09%) |
| データなし(CI と同じ条件) | 50 runs・7 skips | 184 / 220(83.63%) |
同じコードのまま 10 ポイント動きます。ファイル別に見ると、動いたのは実データを読むところです(coverage/.resultset.json から集計しました)。
| ファイル | データなし | データあり |
|---|---|---|
lib/getting_started_ml/chapter03.rb |
12 / 22(54.55%) | 22 / 22(100.00%) |
lib/getting_started_ml/chapter03/rumale_tree.rb |
10 / 15(66.67%) | 15 / 15(100.00%) |
lib/getting_started_ml/chapter02/table.rb |
21 / 25(84.00%) | 24 / 25(96.00%) |
lib/getting_started_ml/chapter02/preprocessing.rb |
37 / 42(88.10%) | 42 / 42(100.00%) |
lib/getting_started_ml/chapter01.rb |
30 / 35(85.71%) | 30 / 35(85.71%) |
lib/getting_started_ml/chapter02.rb |
9 / 16(56.25%) | 9 / 16(56.25%) |
chapter03.rb が 54.55% から 100% に上がるのは、test/iris_tree_test.rb に「実行すると深さごとの正解率と決定木を表示する」というテストがあり、データがあるときだけ第 3 章の run を最後まで通すからです。逆に chapter02.rb はどちらも 56.25% で、第 2 章の run を通すテストがまだ無いことが分かります。
Ruby 版では、実データのテストが走ったかどうかは skips の件数で分かります(第 4 章)。カバレッジの差は、どの行が実データのテストでしか通っていないか を教えてくれます。データなしで 0% にならない行は、単体テストが架空の値で通しているということです。
カバレッジに下限を設けて CI で強制する運用にするなら、どちらの条件で測った数字なのかを決めておかないと意味がありません。SimpleCov には minimum_coverage で下限を設ける機能がありますが、本シリーズは CI に学習データを置けない(再配布できない)ので、下限は設けず、数字は傾向を見るために使います。
coverage/ は第 4 章の .gitignore で除外済みです。
5.7 まとめ¶
この章では、Ruby の依存と品質の道具立てを見ました。
- Bundler —
Gemfile(Ruby の DSL)に直接の依存を、Gemfile.lockに解決済みの版とプラットフォームを記録する。bundle install・bundle outdated・bundle update <gem>で依存を操作する。lock にチェックサムは無い - 悲観的バージョン制約 —
"~> 2.2"は>= 2.2, < 3.0、"~> 2.2.0"は>= 2.2.0, < 2.3。書いた桁数で範囲が決まる - 本番依存と開発依存 —
group :development, :testで分け、BUNDLE_WITHOUTで除ける - Ruby の版 —
Gemfileの下限・lock の記録・.ruby-versionの 3 か所に書かれ、実際には Nix が 3.3.10 を用意する。乱数の並びもこれで固定される - numo-narray-alt — 本家の numo-narray は 2022 年で止まり、Rumale 2.x は fork の numo-narray-alt に依存する。numo 系は
Gemfileに直接書かず Rumale に任せる - RuboCop — 整形と静的解析を 1 回で検査する。
NewCops: enableを書かないと 163 個の cop について警告が出る。外す cop はtest/に限り、理由をコメントに残す - SimpleCov — 行カバレッジを測る。学習データの有無で 94.09% と 83.63% に分かれるので、どの条件で測った数字かを明示する
次の章では、これらの検査を Rake と Gulp の 1 つのタスクにまとめ、GitHub Actions で自動的に走らせます。そこで、Nix の環境の中で bundle exec が Gemfile.lock と違う版の gem を読み込んでいた、という落とし穴も見ます。