@ledsun blog

無味の味は佳境に入らざればすなわち知れず

Node.jsでruby.wasmを動かす時にRubyスクリプトをどうやって読み込むか?

私はこんな関数を書きました。

async function requireRubyScript(vm, path) {
 vm.eval(await readFile(path, "utf-8"));
}

await requireRubyScript(vm, "calculator.rb");な風に使います。 これはデフォルトで欲しいですよね。 例えば

vm.require("calculator.rb");

でも、requireだとgemが読み込める気がしませんか? 今のところはもっとシンプルに実行ファイル(例えばindex.mjs)からの相対パスでファイルが読み込めたら十分です。

vm.load("calculator.rb");

ではどうでしょうか? Kernel.#loadの探索はもっと複雑なので、誤解を招きそうです。

vm.evalFile("calculator.rb");

くらいがよさそうです。

名前が決まったら、次はNode.jsのときだけ有効になるメソッド実装方法が気になってきます。 んー、もしかしてブラウザでもNode.jsでも動くようにしたら良いのかな?

2025年のko.rb

2025年を振り返ってみたら、今年、ko.rbをはじめてました。 去年の出来事だと思い込んでいました。 今年の前半の記憶が曖昧です。 2025年のko.rbのあゆみを振り返ってみたいと思います。

Day 0

2025年1月18日 東京Ruby会議12で「Regional.rb and the Tokyo Metropolis」という地域のRubyコミュニティにフォーカスしたセッションがありました。 刺激を受けた男4人がオーガナイザーになり、ko.rbをはじめることにしました。 のちにいう(いわない)「横浜市鶴見区の誓い」です。

タイムライン

P 0

2025年2月10日 企画会議 男魚魚 (オットット)のご予約 - 千歳烏山/居酒屋 | 食べログ

オーガナイザーで集まってあらためて気持ちを確認しました。

P1

2025年3月19日 企画会議 原始焼 日本酒 雨のち晴レルヤ。 - 千歳烏山/居酒屋 | 食べログ

初回日程とコンテンツを決めました。

#0

2025年4月28日 【オフライン開催】ko.rb #0 - connpass

RubyKaigiで勧誘したため盛況でした。 勧誘した本人はこの日はまだ愛媛にいたようです。

P2

2025年6月16日 企画会議 アミレバ東京 - 八幡山/もつ焼き | 食べログ

第2回を8月に決めました。

#1

2025年8月25日 【オフライン開催】ko.rb #1 - connpass

P3

2025年10月6日 企画会議 ダイニングバー KUU (クウ)のご予約 - 明大前/ダイニングバー | 食べログ

次回を12月頭に決めました。

P4

2025年12月26日 企画会議 酒場アカボシのご予約 - 千歳烏山/居酒屋 | 食べログ

なんだかんだで12月は開催できませんでした。 来年にむけて仕切り直しです。

ふりかえる

2025年をふりかえって

開催回数が2回でした。 もう少し多いと良かったかな?と思います。

2026年に向けて

偶数月定期開催を目指します。 コンテンツを工夫するより、定期開催を重視していきます。

Node.jsからruby.wasm上のRubyに値を渡す

Node.jsからruby.wasmを使ってRubyのメソッドを呼ぶ - @ledsun blog の続きです。

vm.eval

例えば計算機クラスがあるとしたらこんな感じです。

const calculator = vm.eval('Calculator.new');
const value1 = vm.eval("1");
const value2 = vm.eval("2");
const result = calculator.call("add", value1, value2);
result.toString();
// => 文字列の3が得られます。

vm.eval("1")してRubyの値を作る必要があります。 実はruby.wasmでRubyからJavaScriptを呼ぶ時、Rubyの値をJavaScriptの値へ変換する時も内部ではnew_array = JS.eval("return []")のようなことをしています。 https://github.com/ruby/ruby.wasm/blob/c5838ac9848de64c1b7b220522de83a74f9c0621/packages/gems/js/lib/js/array.rb

つまりこれはインターフェースデザインの問題です。 例えば、1.toRuby() みたいに変換できればよさそうです。 しかし、JavaScriptのnumber型にメソッドを生やすのは難しそうです。

vm.toRuby(1)があればいいのでしょうか? それならcalculator.call("add", 1, 2)と呼んで内部で変換してもらえるのがいいですよね。 内部でつかうならvm.numberToRuby(1)のような細かい関数の方が保守しやすそうです。

vm.wrap

配列だと少し趣が異なります。

const ary = vm.eval("[1, 2, 3]");
ary.call("push", vm.wrap(4));
const value = ary.call("pop");
value.toJS();
// => JavaScriptの3が得られます 

参照だけ渡してRuby側で値を変更しない場合はvm.wrap(4)でRbValueにできます。 元に戻すにはtoJS()を呼びます。

欲しいもの

JavaScriptの値をRubyの値に変換する処理があるとよさそうです。

  • 数値(Number)
  • 文字列(String)
  • 真偽値(Boolean)
  • null

あたりは自動的に変換されてほしいです。

undefinedはどうなるのでしょう?Ruby側で扱えないので無視して良さそうです。 扱おうにも「引数を指定されいない」のか「引数にundefinedが指定されている」のか区別できないので厳しそうです。

  • 配列
  • オブジェクト

もあるとよさそうです。 オブジェクトをハッシュに変換できるか判定するのは難しそうです。 RbValueでラップしてインデックスアクセスできるようにするといいかもしれません。

Node.jsからruby.wasmを使ってRubyのメソッドを呼ぶ

Node.jsからruby.wasmを動かす - @ledsun blog の続きです。

index.mjsを次のように書きます。

import { readFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { DefaultRubyVM } from "@ruby/wasm-wasi/dist/node";
const vm = await initRubyVM();
const script = `
class Dog
 def name()= 'jhon'
end
Dog.new
`
const dog = vm.eval(script);
const result = dog.call("name");
console.log(result.toString());
async function initRubyVM() {
 const require = createRequire(import.meta.url);
 const rubyWasmPath = require.resolve("@ruby/4.0-wasm-wasi/dist/ruby.wasm");
 const binary = await readFile(rubyWasmPath);
 const module = await WebAssembly.compile(binary);
 const { vm } = await DefaultRubyVM(module);
 return vm;
}

dog.call("name")でメソッドが呼び出せます。

Rubyのオブジェクトは、JavaScriptからはRbValueクラスのインスタンスとして見えます。 RbValueクラスにはcallメソッドが定義されています。 名前を指定して任意のメソッドが呼び出せます。

dog.name()のようなスタイルでは呼び出せません。

Node.jsからruby.wasmを動かす

とてもひさしぶりです。 リハビリがてらとても簡単な例を動かします。 Ruby1 + 1を計算します。

npm i @ruby/wasm-wasi @ruby/4.0-wasm-wasi

index.mjsを作ります。

import { readFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { DefaultRubyVM } from "@ruby/wasm-wasi/dist/node";
const require = createRequire(import.meta.url);
// npm パッケージ内の ruby.wasm を解決して読み込む
const rubyWasmPath = require.resolve("@ruby/4.0-wasm-wasi/dist/ruby.wasm");
const binary = await readFile(rubyWasmPath);
// Wasm をコンパイルして Ruby VM を初期化
const module = await WebAssembly.compile(binary);
const { vm } = await DefaultRubyVM(module);
// Ruby を eval して結果を受け取る
const result = vm.eval("1 + 1");
// RbValue を文字列化して表示("2" が出ます)
console.log(result.toString());

vm.evalメソッドで任意のRuby スクリプトを実行できます。

vm.evalメソッドは https://github.com/ruby/ruby.wasm/blob/c5838ac9848de64c1b7b220522de83a74f9c0621/packages/npm-packages/ruby-wasm-wasi/src/vm.ts#L592-L594 で定義されています。

RubyVMクラスを拡張すると新しい機能を追加できます。

マインクラフトのローカルサーバを立ててPS5とSwitchからマルチプレイする

前提情報

  • マインクラフトでネットワーク接続するにはマイクロソフトアカウントが必要
  • 一つのサーバーに同時に接続するには、人数分のマイクロソフトアカウントが必要
  • PS5とNintendo Switchはネットワーク機能を使うにはサブスク加入が必要
  • LAN内のサーバーへの接続は公式にはサポートされていない

作業項目

注意点

BedrockConnectを使ってもサーバーリストに出てくるサーバーの名前は変わりません。 サブタイトルに「Join To Open Serve...」と表示されます。 ですが気がつきにくいです。

設定が上手く行っていることに気がつかなくて苦戦しました。

VPN接続するとWSL内のDockerコンテナからのapt-get updateに失敗する

WSL

VPN接続するとWSLからのHTTPS接続に失敗する - @ledsun blog の続きです。 WSL上でのHTTPS接続には成功しましたが、Dockerを使うと同様のMTU問題にはまります。

現象

例えば次のコマンドに長時間かかりタイムアウトします。

docker run -it --rm ruby:3.4.7-slim bash -lc 'apt-get update -qq'

原因

ホストOSにMTUを設定しても、dockerネットワークには反映されません。

> ip link show docker0
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
 link/ether f6:5a:59:94:de:23 brd ff:ff:ff:ff:ff:ff

対策

DockerにもMTUを設定します。 /etc/docker/daemon.json に設定を書きます。

{
 "mtu": 1350
}

Dockerの再起動が必要な点に注意してください。 設定はすぐには反映されません。

> sudo systemctl restart docker

再起動後はMTUが変更されています。

> ip link show docker0
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1350 qdisc noqueue state UP mode DEFAULT group default
 link/ether f6:5a:59:94:de:23 brd ff:ff:ff:ff:ff:ff

VPN接続するとWSLからのHTTPS接続に失敗する

WSL

現象

WindowsVPN接続をして、WSLからHTTPS接続しようとすると

►curl -v https://www.google.com/ 2>&1 | head -n 20
* IPv6: 2404:6800:4004:81d::2004
* IPv4: 142.250.193.196
* Trying 142.250.193.196:443...
* Connected to www.google.com (142.250.193.196) port 443
* ALPN: curl offers h2,http/1.1
} [5 bytes data]
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
} [512 bytes data]
* CAfile: /etc/ssl/certs/ca-certificates.crt
* CApath: /etc/ssl/certs

で固まります。

VPN接続していなければ繋がります。 また、PowerShellcurl -v https://www.google.com/は通ります。 どうやらWSLとVPNの組み合わせに何かあるようです。

原因

sudo ip link set dev eth0 mtu 1350を実行すると通るようになります。 MTUの問題のようです。

以下、私のおおざっぱな理解です。

Windows上のVPNクライアントがVPN接続しています。 WSLはVPNの存在を知りません。 WSLは1500バイトでパケットを送ります。 Windows上のVPNクライアントはカプセル化します。 1500を超えるためパケットは分割されます。

通信経路上のいずれかの機器が分割されたパケットを拒否する可能性があります。 また機器がパケット分割を回避するためICMP Frag-Neededを返しているのかもしれません。 が、WindowsのNATが中継しないためか、WSLは適切なパケットサイズで送っているつもりで理解できないためか検知できないようです。

そこで手動でMTUを設定します。 VPN カプセル化してもパケット分割が起きなくなります。

対処

永続化するにはWSLの /etc/wsl.conf に設定します。

[network]
mtu=1350

確認するには、 Windows側でwsl --shutdownしてWSLを再起動します。

MTU探訪

実際は何が障害なのでしょうか?

Windows VPNなし

ます、WindowsからMTUを計測してみます。 ping google.com -f -l 1432コマンドを使います。

PS C:\Windows\System32> ping google.com -f -l 1432
google.com [172.217.31.174]に ping を送信しています 1432 バイトのデータ:
172.217.31.174 からの応答: バイト数 =1432 時間 =6ms TTL=116
172.217.31.174 からの応答: バイト数 =1432 時間 =8ms TTL=116
172.217.31.174 の ping 統計:
 パケット数: 送信 = 2、受信 = 2、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
 最小 = 6ms、最大 = 8ms、平均 = 7ms
Ctrl+C
PS C:\Windows\System32> ping google.com -f -l 1433
google.com [172.217.31.174]に ping を送信しています 1433 バイトのデータ:
192.168.1.1 からの応答: パケットの断片化が必要ですが、DF が設定されています。

1432バイトが限界です。これはIPヘッダー20バイトとICMPヘッダ8バイトを足した1460バイトです。 これがVPNを使っていない状態のMTU最適値です。

Windows VPNあり

次に、VPNに接続すると

PS C:\Windows\System32> ping google.com -f -l 1323
google.com [142.250.196.142]に ping を送信しています 1323 バイトのデータ:
パケットの断片化が必要ですが、DF が設定されています。
パケットの断片化が必要ですが、DF が設定されています。
142.250.196.142 の ping 統計:
 パケット数: 送信 = 2、受信 = 0、損失 = 2 (100% の損失)、
Ctrl+C
PS C:\Windows\System32> ping google.com -f -l 1322
google.com [142.250.196.142]に ping を送信しています 1322 バイトのデータ:
要求がタイムアウトしました。
要求がタイムアウトしました。
要求がタイムアウトしました。
要求がタイムアウトしました。
142.250.196.142 の ping 統計:
 パケット数: 送信 = 4、受信 = 0、損失 = 4 (100% の損失)、

1322バイトが限界です。 IPヘッダとICMPヘッダを足して1350バイトです。 1350がVPN接続時のMTU最適値と見て良さそうです。

まあ、でもタイムアウト気になりますよね。

PS C:\Windows\System32> ping google.com -f -l 1000
google.com [172.217.31.174]に ping を送信しています 1000 バイトのデータ:
172.217.31.174 からの応答: バイト数 =1000 時間 =51ms TTL=116
172.217.31.174 からの応答: バイト数 =1000 時間 =20ms TTL=116
172.217.31.174 の ping 統計:
 パケット数: 送信 = 2、受信 = 2、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
 最小 = 20ms、最大 = 51ms、平均 = 35ms
Ctrl+C
PS C:\Windows\System32> ping google.com -f -l 1001
google.com [172.217.31.174]に ping を送信しています 1001 バイトのデータ:
要求がタイムアウトしました。
要求がタイムアウトしました。
要求がタイムアウトしました。
要求がタイムアウトしました。
172.217.31.174 の ping 統計:
 パケット数: 送信 = 4、受信 = 0、損失 = 4 (100% の損失)、

1000バイト境界で何かいます。 謎の挙動です。

WSL VPNなし

sudo ip link set dev eth0 mtu 1500を実行して、MTUを1500に戻しておきます。

Linuxでは、ping -M do -s 1432 google.comコマンドを使います。

ledsun@xps24nov:/m/c/U/led_l►ping -M do -s 1432 google.com
PING google.com (142.250.193.206) 1432(1460) bytes of data.
1440 bytes from del11s17-in-f14.1e100.net (142.250.193.206): icmp_seq=1 ttl=115 time=7.75 ms
1440 bytes from del11s17-in-f14.1e100.net (142.250.193.206): icmp_seq=2 ttl=115 time=10.6 ms
^C
--- google.com ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1004ms
rtt min/avg/max/mdev = 7.747/9.154/10.561/1.407 ms
ledsun@xps24nov:/m/c/U/led_l►ping -M do -s 1433 google.com
PING google.com (142.250.194.174) 1433(1461) bytes of data.
From ntt.setup (192.168.1.1) icmp_seq=1 Frag needed and DF set (mtu = 1460)
ping: local error: message too long, mtu=1460
^C
--- google.com ping statistics ---
2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 1000ms

1432境界です。

WSL VPNあり

1322境界があると思うじゃないですか?

ledsun@xps24nov:/m/c/U/led_l[1]►ping -M do -s 1323 google.com
PING google.com (172.217.31.174) 1333(1361) bytes of data.

で固まります。 つまりICMP Frag-Neededは受け取れません。

1000の境界を試すと

ledsun@xps24nov:/m/c/U/led_l[1]►ping -M do -s 1000 google.com
PING google.com (142.250.194.174) 1000(1028) bytes of data.
1008 bytes from del12s06-in-f14.1e100.net (142.250.194.174): icmp_seq=1 ttl=115 time=21.5 ms
1008 bytes from del12s06-in-f14.1e100.net (142.250.194.174): icmp_seq=2 ttl=115 time=21.5 ms
1008 bytes from del12s06-in-f14.1e100.net (142.250.194.174): icmp_seq=3 ttl=115 time=22.9 ms
1008 bytes from del12s06-in-f14.1e100.net (142.250.194.174): icmp_seq=4 ttl=115 time=26.1 ms
1008 bytes from del12s06-in-f14.1e100.net (142.250.194.174): icmp_seq=5 ttl=115 time=24.5 ms
^C
--- google.com ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4005ms
rtt min/avg/max/mdev = 21.459/23.273/26.108/1.797 ms
ledsun@xps24nov:/m/c/U/led_l►ping -M do -s 1001 google.com
PING google.com (142.250.194.174) 1001(1029) bytes of data.

これはWindowsと一致しています。 ICMP Frag-Neededが受け取れないのはWindowsと一致していません。 WSL固有の現象のようです。

どうやらWindowsのNATがそういう性質らしいです。よくわかりません。

また、いまのところMTUは1350が良さそうです。 ですが1000バイト境界の謎挙動も気になる所です。

strong parameterのexpectで空文字はエラー

空文字を許容するのかしないのか混乱したので、メモっておきます。

my-app(dev):002> params = ActionController::Parameters.new(a: "")
=> #<ActionController::Parameters {"a" => ""} permitted: false>
my-app:003(dev)> params.expect :a
(my-app):3:in '<main>': param is missing or the value is empty or invalid: a (ActionController::ParameterMissing)

require と同じ動きです。

my-app(dev):004> params.require :a
(my-app):4:in '<main>': param is missing or the value is empty or invalid: a (ActionController::ParameterMissing)

SQLite3でridgepoleを動かす

GitHub - ridgepole/ridgepole: Ridgepole is a tool to manage DB schema. It defines DB schema using Rails DSL, and updates DB schema according to DSL. (like Chef/Puppet) はSQLite3をサポートしていません。

READMEに0.6.0以降 Disable sqlite support と明記されています。 まあ、でもRails 8以降SQLiteが推されてますし、SQLite3にも使いたいじゃないですか。 今日は、雑にどんな修正が必要そうか考えてみます。

対応するオプション

SQLite3はadd_column メソッドのうち次のオプションが使えません。

  • after
  • null

他にもあるかもしれません。 今回はこの対応だけを考えます。

準備

SQLite3で分岐するために https://github.com/ridgepole/ridgepole/blob/ff299198a20fbfdc51e8779bb790c923cb98dd8c/lib/ridgepole/connection_adapters.rb にSQLite3判定メソッドを追加します。

module Ridgepole
 class ConnectionAdapters
 def self.mysql?
 defined?(ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter) && ActiveRecord::Base.connection.is_a?(ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter)
 end
 def self.postgresql?
 defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) && ActiveRecord::Base.connection.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
 end
 def self.sqlite3?
 defined?(ActiveRecord::ConnectionAdapters::SQLite3Adapter) && ActiveRecord::Base.connection.is_a?(ActiveRecord::ConnectionAdapters::SQLite3Adapter)
 end
 end
end

afterオプション

https://github.com/ridgepole/ridgepole/blob/ff299198a20fbfdc51e8779bb790c923cb98dd8c/lib/ridgepole/diff.rb#L274-L278afterオプションを追加しています。 まるごと除外します。

unless Ridgepole::ConnectionAdapters.sqlite3?
 if priv_column_name
 to_attrs[:options][:after] = priv_column_name
 else
 to_attrs[:options][:first] = true
 end
end

null オプション

https://github.com/ridgepole/ridgepole/blob/ff299198a20fbfdc51e8779bb790c923cb98dd8c/lib/ridgepole/dsl_parser/table_definition.rb#L96 でSchemefileのカラム定義に書いてあるオプションをを読み込んでいます。 ここでnullオプションを無視します。

define_method column_type do |*args|
 options = args.extract_options!
 if Ridgepole::ConnectionAdapters.sqlite3?
 options.delete(:null)
 end
 options = default_options.merge(options)
 column_names = args
 column_names.each { |name| column(name, column_type, options) }
end

修正案を書きましたが、そもそもSchemefileにnullオプションを書かない方が良さそうに思います。

まとめ

とりあえずSQLite3でもridgepoleは使えます。

メンテナンスする自信が無いのに、サポート範囲を広げるを投げるのは、メンテナの負荷を増やしすぎる問題があります。 しばらく手元で動かしてみて、他に問題が出ないか試してみます。

2025年11月16日 追記

SQLiteは非null制約をサポートしています。 マイグレートに失敗したのは、既にnullが入っている行があったからでした。 勘違いしていました。

ClickOnceアプリケーションのインストールエラーと修復

Window App SDKに一切依存していない、.Net Framework 4.8 のWindows FormsアプリケーションのClickOnceインストールでエラーが出ました。

詳細をみると

Microsoft.UI.Windowing.Core.dll が見つからないため、コードの実行を続行できません。プログラムを再インストールすると、この問題が解決する可能性があります。

「アプリと機能」を見ると、それっぽい名前の「Microsoft Windows App Runtime Singleton」が11日前に更新されていました。 「修復」したら直りました。

ChatGPTが考える「AI導入成熟度モデル(AIMM)」

これはChatGPTにCMMを参考にしてAI導入レベルを書かせた怪文書です。 見出しを絵文字で修飾するのがChatGPTっぽいです。

レベル1〜3は想像より、的を射ていて面白いです。 レベル4以上は、現実世界ではまだ実現できていません。 レベル5が達成できるとしたら、夢があります。


🧭 AI導入成熟度モデル(AIMM)

AI Adoption Maturity Model
― 組織はどの段階でAIと共に学んでいるのか ―

📘 概要

AI導入は、もはや一部の先進企業だけの試みではなく、あらゆる組織にとって避けられない経営テーマとなっている。 しかし、その多くは、単なるツール導入にとどまり、「生産性向上」「業務効率化」という言葉の背後にある組織の学習能力の変化を捉えきれていない。

AI Adoption Maturity Model(AIMM)は、組織がAIをどのように受容・定着・再定義していくかを5段階で示す枠組みである。 本モデルはCMM(Capability Maturity Model)の思想を踏まえ、AI導入を単なる技術投資ではなく、知的成熟のプロセスとして再定義する。

🥇 レベル1:Ad-hoc(場当たり的導入)

概要

個々の社員が自主的にAIツールを試用する段階。
導入は散発的で、組織としての戦略や評価軸は存在しない。

特徴

  • 個人がChatGPTやClaude等を独自に利用
  • 成果共有・ナレッジ化が行われていない
  • 「便利そうだから」という動機
  • ガバナンス・セキュリティ未整備

リスク

  • 属人化の加速
  • 効果が可視化されない

代表的フレーズ

「とりあえずPro課金してみた」

🥈 レベル2:Repeatable(再現可能な導入)

概要

一部部署やプロジェクト単位でAIツールを正式導入し、部分的な成果を得る段階。
PoC(概念実証)が多く、"導入実績"を重視する傾向にある。

特徴

  • 定型業務(議事録、ドキュメント要約、コードレビューなど)での利用
  • 成果は限定的だが、「AI導入済み」としての評価が社内外に広がる
  • KPIは「工数削減」「便利さ」など定性的

リスク

  • 効果測定が曖昧なまま投資が拡大
  • "PoC疲れ"による形骸化

代表的フレーズ

「AI導入でしろまる時間削減!」

🥉 レベル3:Defined(定義された運用)

概要

AI活用がプロセスとして定義され、全社的なルール・ガイドラインが整備される段階。
利用方針・instruction・教育プログラムが文書化されるが、実運用は追いついていない場合が多い。

特徴

  • 利用ルール・instructionを正式策定
  • 社内ガイドラインや勉強会が整備される
  • 部署ごとの運用が標準化されるが、形式的になりがち

リスク

  • 「ルールを作ること」自体が目的化
  • 本質的なスキル育成やプロセス改善に結びつかない

代表的フレーズ

「AI利用方針はありますが、現場では誰も見ていません」

🏗 レベル4:Managed(効果が計測・管理される)

概要

AI導入の成果が数値化され、経営層が意思決定指標として扱う段階。
AIが業務基盤に統合され、業務プロセスの一部として運用される。

特徴

  • PR処理時間・ドキュメント作成時間・問い合わせ応答時間などがKPI化
  • instruction更新・モニタリング体制が確立
  • データガバナンスと権限管理が制度化
  • "AIエージェント"が日常業務に常駐

リスク

  • 生産性の定量化が目的化し、「思考の深さ」が犠牲になる。
  • AIへの依存が進み、"AIなしで仕事ができない人"が生まれ始める。

代表的フレーズ

「AIエージェントなしでは業務が回らない」

🧠 レベル5:Optimizing(共進化と再自立)

概要

AIを単なる補助ではなく、知的共創パートナーとして再定義する段階。
AIエージェントがOJTの一部を代行し、人間は創造・内省・設計へと再集中する。

特徴

  • AI活用を通じた組織知の循環が確立
  • AI instructionを人間が再教育する文化が生まれる
  • 「AI依存」から「AI共進化」へ移行
  • 脱AIリテラシー教育(AIなしでの思考訓練)を重視

成熟した組織の姿

  • AIを"業務効率化の道具"ではなく、"知的筋力を鍛える装置"として位置づける
  • 「AIがあるからこそ、AIに頼らず考える」文化が定着する

代表的フレーズ

「AIに教えられる人ではなく、AIを教育できる人を育てる」

🪜 レベル別サマリー

レベル 名称 主な特徴 組織の姿勢
1 Ad-hoc 個人で使うだけ 興味本位
2 Repeatable 試験導入 宣伝・PoC志向
3 Defined ルール整備 管理と形式化
4 Managed 効果測定 経営主導の最適化
5 Optimizing 共進化 自律的再設計・文化化

🎯 結論

AI導入の成熟度は「技術的スキル」ではなく、
組織が"知的自立"をどこまで維持できるかで決まる。

つまり、最終段階(レベル5)では、
AI導入とは「AIに頼らない力を取り戻すためのプロセス」である。

AIに頼らず、AIを通じて人間を鍛える文化をつくること。
それが真に成熟したAI組織の姿である。

AIエージェントよりAIペアプロが好き

はじめに

小さなツールをつくる機会があり、その中の1ステップの仕様が明確になったので、AIエージェントを使って実装してみました。

自分で書きはじめた方が速い

想像以上に遅く感じました。 実装の場所も方法も決まっているのに、AIエージェントが調べている間じっと待っているのがストレスでした。 AIエージェントのレスポンスを待つよりも、自分でコードを書き始めてCopilotに補完してもらう方が速く感じます。

私が生成AIに求めているのは「作業代行」でなく「ペアプロの相手」のようです。

AIエージェントの使い方?

「設計を全部終わらしてからAIエージェントに頼む」は、AIエージェントの上手い使い方ではなかったのかもしれません。 個人的には「いまの生成AIからはクラス設計のセンスを感じられない。内部設計は任せたくない...」と感じています。 それで設計まで終わらせて実装をお願いしました。

たとえば、タスクのゴールまで決めて、会議中に裏で「このタスクの実装やっといて」と任せる使い方の方がよいのかもしれません。

タスク単位で依頼する時は、人間のエンジニア相手でも難しさがあります。 依頼したタスクが想定よりも時間がかかることがあります。 そういうときはエンジニアのスキル不足より「依頼の仕方が間違っている」ことが多いです。 よく見てみると、簡単なつもりで難しいタスクを依頼していたり、そもそも間違った方針を伝えていたりします。 「お願いする側のタスク設計の精度」が作業効率に影響します。 AIエージェントに対しても似たことがおきそうです。

人間のエンジニアは学習します。 無茶振りしていると「この依頼者は適当なことを言う」と学習してくれます。 期待しすぎるとパワハラになりますが、この期待は0ではないです。 最近の生成AIは忖度しがちです。 負のフィードバックを得にくいかもしれません。

人間よりも生成AIのほうが依頼前の「完遂できるタスク」の準備に気を遣いそうです。

AIペアプロの価値

人間とペアプロするとめちゃくちゃ疲れます。 プログラミングについて深く考えているからという面もありますが、気にすることが多過ぎます。

  • 前提を共有できているか?
  • この言い方で通じるか?
  • 相手の理解と自分の理解がどう違うのか?
  • ドライバーとナビゲーターをいつ交代すればいいのか?

これらをずっと考え続けながらプログラミングのことを考えます。 開発全体を考えると背景情報を共有する効果があります。 プログラミングを目的としたら非効率です。

AIとのペアプロなら、人間を相手にするときの気疲れがありません。 試行錯誤を気軽に続けられます。 「これはたぶんダメなアイデアなんだけど、軽く試してダメなことを確認しておきたい」ときも、ペアの気分を気にせずに試せます。

おわりに

今回の試行では「僕なりのAIエージェントの使い方」は見つけられませんでした。

AIは学習しません。タスクをこなしても経験が蓄積されません。 ジュニアエンジニアは成長し、長期的にはチームに貢献してくれます。 生成AIはモデルは成長していくのですが、誰もが同じモデルを使えます。 エンジニアリング組織の差別化には使えません。

世の中では、相対的にジュニアエンジニアが割高に見えているはずです。 ジュニアエンジニアはAIエージェントと比べたら20〜30倍の費用が掛かるので気軽に雇えません。 いまの「ジュニアエンジニアを雇える立場」は「ジュニアエンジニアを育成できる環境」ともいえます。 このままAIエージェントが流行ってくれれば「ジュニアエンジニアを雇って、育てられる」が、エンジニアリング組織の参入障壁として機能し、差別化要因になりそうです。

さて勝ち筋はあるかな?

Kaigi on Rails 2025 参加メモ(Day2)

小規模から中規模開発へ、構造化ログからはじめる信頼性の担保(kakudooo)

内容

小規模から中規模へ 構造化ログからはじめる信頼性の担保 - Speaker Deck

感想

  • ステップの説明が丁寧で理解しやすかった
  • 技術選定するときに候補を3つ挙げるのが良い
  • 無理にフックを仕込まなくても面白い発表でした

参考リンク

Range on Rails ― 「多重範囲型」という新たな選択肢が、複雑ロジックを劇的にシンプルにしたワケ(梅田智大)

内容

  • PostgreSQL 14 以降で使える多重範囲型(multirange)を紹介
  • 予約システムの予約枠は範囲
  • 予約枠(範囲)に集合演算できると「空き枠」を探すときにめっちゃ便利
  • SQLビューでラップするとRailsからは透明に使える

感想

  • 知らない技術を実用例込みで知れて勉強になった
  • Pure Rubyの多重範囲型を演算できるライブラリーを作ったら面白そう

非同期処理実行基盤、Delayed脱出〜SolidQueue完全移行への旅路。(Shohei Kobayashi)

内容

非同期処理実行基盤 Delayed脱出 → Solid Queue完全移行への旅路。 - Speaker Deck

  • Delayed Job から Solid Queue** への移行事例
  • Rails 7.2 の enqueue_after_transaction の有効性
  • 監視機能が内のを自作でカバー

感想

  • 教科書のようなSolid Queueの紹介事例
  • 最初に扱う問題領域を明確にしているのが良い
    非同期処理の分類を説明するスライド
  • Solid Queueを使おうかな?って思った人は、一回は読むと良さそう

Railsだからできる、例外業務に禍根を残さない設定設計パターン(ei.ei.eiichi)

内容

  • 「坂の途中」での例外的な業務ロジックへの対応方法を紹介

感想

  • キーノートのmoroさんwayを地で行く感じで良かったです
  • 速すぎておいていかれたので、資料を公開してほしいです
  • 「UNDO機能が無くても履歴を見せよう」って発想が参考になりました

ドメイン指定Cookieとサービス間共有Redisで作る認証基盤サービス(黒曜)

内容

ドメイン指定Cookieとサービス間共有Redisで作る認証基盤サービス

感想

  • 解きたい課題が「すでにあるサービスに認証基盤を追加する」で面白かった
  • Redis Session Storeを流用するのと、認証基盤に認可情報を返すAPIを足すのはどっちが楽なんだろう?

参考リンク

Rails on SQLite: exciting new ways to cause outages(André Arko)

内容

Rails on SQLite: exciting new ways to cause outages - Speaker Deck

  • 会場では全然分かりませんでした
  • Rails on SQLite: exciting new ways to cause outagesスクリプトがあります
  • feedyour.emailで得られたRails on SQLiteの全知見大公開
  • 月100万リクエストに耐えている
  • SQLiteのファイルを複数プロセスで共有するのはむずかしい
    • バックグラウンドジョブはスレッド(litejob)で処理する
  • SQLiteは1ファイルなので書き込みが読込をブロックする
    • WAL(Write Ahead Logging)オプションを使うと読込ブロックを解消出来る
  • 書き込み同士はブロックする
    • Railsのキャッシュやジョブなどでファイルをわけていくアプローチが有効
  • 原則、水平スケーリングはできないので垂直スケーリングしていくこと
    • 現代はハイスペックなサーバーインスタンスが使えるので1台でも結構がんばれる
  • 1インスタンスなので壊れたら止まる
    • Litestremを使えばリアルタイムにバックアップがとれる
    • 壊れた時点のデータで復元できる
  • 1プロセスなので入り口の負荷分散は難しい
    • CDNキャッシュは有効
  • 1サーバーだと保守・監視が楽

感想

参考

rails g authenticationから学ぶRails8.0時代の認証(Willnet)

内容

rails g authenticationから学ぶRails8.0時代の認証 - Speaker Deck

  • Rails 8で導入された認証ジェネレーターの紹介
  • Devise / Warden に頼らない
  • メールアドレス+パスワードの基本形のみ提供
  • generatorであり、gemではない。
    • 脆弱性が出たらgem のアップデートでは終わらない
  • has_secure_passwordCurrentAttributes の活用例として参考になる
  • ActionCable用の認証コードも生成される

感想

  • BCrypt の「遅くできる」設計が面白い
  • タイミングアタックはふつうは、攻撃方法の存在に気づけないよなあ
  • お子さんが応援されているの、ほっこりして良かったです

Keynote: Building and Deploying Interactive Rails Applications with Falcon(Samuel)

内容

Aysncからの歴史を含んだFalconの紹介

  • つぎつぎと紹介される自作Gemの数々
  • Agant csntextで生成したAIエージェントむけインストラクションをつかったライブでも

感想

  • RubyにAsync入れたい」からはじめて必要なものを全部作ってFalconつくってShopify入って実運用して数億円節約して価値を示す技術力で全部ぶっとばしていくスタイルがエンジニアとしてかっこよすぎる
  • 発表中に紹介されるgemが全部自作なのウケる
    • すごすぎて一回宇宙猫になったあとウケるしかなくなる

参考

🎤 Closing

  • 来年は ベルサール渋谷ガーデン で開催予定
  • 参加者1,000人規模。
  • 2025年10月16〜17日
  • より国際化を進める

devContainer環境作成失敗 解決編

devContainer環境作成失敗 - @ledsun blog の続きです。

snapの/tmpはprivate

ledsun@xps24nov:~/devContainerTest►docker compose --project-name devcontainertest_devcontainer -f /home/ledsun/devContainerTest/.devcon
tainer/docker-compose.yml -f /tmp/devcontainercli-ledsun/docker-compose/docker-compose.devcontainer.containerFeatures-1759046001713-5c1
ef8d1-864d-4f75-93a2-7852df5b90e8.yml up -d
open /tmp/devcontainercli-ledsun/docker-compose/docker-compose.devcontainer.containerFeatures-1759046001713-5c1ef8d1-864d-4f75-93a2-7852df5b90e8.yml: no such file or directory

が失敗していました。

open /tmp/(中略).yml: no such file or directory

dockerコマンドから/tmp配下のファイルが見えません。 本当でしょうか?

条件を変えて確認してみましょう。

ledsun@xps24nov:/t/d/docker-compose[1]►snap run --shell docker -c 'ls -l /tmp'
total 0
ledsun@xps24nov:/t/d/docker-compose►ls -l /tmp
total 132
drwxr-xr-x 4 ledsun ledsun 4096 Sep 28 17:15 devcontainercli-ledsun/

やはり/tmp内容が違って見えます。

snapはアプリケーション単位で独立して環境にインストールすることで、セキュリティリスクを軽減する方針のようです。 その一環として/tmpもアプリケーション単位で隔離されているみたいです。

また、DevContainerはsnapをサポートしないことが明示されていました。

vscode-docs/docs/devcontainers/containers.md at 513424f40cb523f4fac0ab4a430684377c36891c · microsoft/vscode-docs · GitHub

Linux: Docker CE/EE 18.06+ and Docker Compose 1.21+. (The Ubuntu snap package is not supported.)

dockerコマンドをインストールし直すと良さそうです。

dockerをaptでインストール

ひとまずアンインストールします。

sudo snap remove docker

インストールし直します。 Ubuntu | Docker Docs を参考にします。

念のため環境をクリーンにします。

for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done

aptリポジトリを追加します。

# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release &&echo"${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable"| \
 sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

インストールを実行します。

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

これで

docker compose --project-name devcontainertest_devcontainer -f /home/ledsun/devContainerTest/.devcon
tainer/docker-compose.yml -f /tmp/devcontainercli-ledsun/docker-compose/docker-compose.devcontainer.containerFeatures-1759046001713-5c1
ef8d1-864d-4f75-93a2-7852df5b90e8.yml up -d

が成功します。

しかし、VSCodeからDevContainer として起動するとエラーが起きます。 docker-copomseの設定が足りていません。

docker-compose.ymlの修正

docker-compose.yml を次のように直します。

services:
 web:
 image: ubuntu:20.04
 command: sleep infinity
 volumes:
 - ..:/workspaces/devcontainer-test

echo "DevContainer test"では、VSCodeが接続する前に終了するので sleep infinity とし、無限に待ち受ちます。

VSCodeがDockerに接続した後に開くワークスペースが必要です。

volumes:
 - ..:/workspaces/devcontainer-test

で、プロジェクトルートをマウントします。

起動

Dev Container: Reopen in Containerに成功したスクリーンショット

引用をストックしました

引用するにはまずログインしてください

引用をストックできませんでした。再度お試しください

限定公開記事のため引用できません。

読者です 読者をやめる 読者になる 読者になる

AltStyle によって変換されたページ (->オリジナル) /