Add Steep type checking for Config
Annotates Config, SentryConfig, and DefaultUser with rbs-inline
types. Adds a Steepfile scoped to config.rb and a `rake check`
task that generates RBS then runs `steep check`. CI runs it
alongside tests.

rbs-inline can't pick up methods defined inside a Data.define
block, so DefaultUser.parse is reopened in a separate class body.

Closes #7

Assisted-by: Claude Opus 4.6 via Claude Code
change kwytmorznnsvvvpvrtsmpqvyloqqmmvm
commit fdd0c86b67ff91d3fc8f30e61fcfa8f655b34692
author Alpha Chen <alpha@kejadlen.dev>
date
parent wwxvszlo
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1e087cf..9117fce 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,7 +18,7 @@ jobs:
       - uses: ruby/setup-ruby@v1
         with:
           bundler-cache: true
-      - run: bundle exec rake test
+      - run: bundle exec rake test check
 
   build:
     needs: test
diff --git a/.gitignore b/.gitignore
index 2e274d9..2ba718e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 db/*.db
+sig/generated/
diff --git a/Rakefile b/Rakefile
index 0c227ff..e267267 100644
--- a/Rakefile
+++ b/Rakefile
@@ -126,4 +126,10 @@ namespace :snapshots do
   end
 end
 
-task default: %i[ test binstubs ]
+desc "Generate RBS from inline annotations and run Steep type checker"
+task :check do
+  sh "rbs-inline --output lib/"
+  sh "steep check"
+end
+
+task default: %i[ test check binstubs ]
diff --git a/Steepfile b/Steepfile
new file mode 100644
index 0000000..0578d34
--- /dev/null
+++ b/Steepfile
@@ -0,0 +1,4 @@
+target :lib do
+  signature "sig"
+  check "lib/ketchup/config.rb"
+end
diff --git a/lib/ketchup/config.rb b/lib/ketchup/config.rb
index b461b39..fd6c128 100644
--- a/lib/ketchup/config.rb
+++ b/lib/ketchup/config.rb
@@ -1,16 +1,36 @@
+# rbs_inline: enabled
 # frozen_string_literal: true
 
-Config = Data.define(:database_url, :sentry, :default_user, :commit_sha, :change_id, :build_date)
+Config = Data.define(
+  :database_url, #: String
+  :sentry, #: SentryConfig?
+  :default_user, #: DefaultUser?
+  :commit_sha, #: String?
+  :change_id, #: String?
+  :build_date, #: String?
+)
 
 class Config
-  SentryConfig = Data.define(:dsn, :env)
-  DefaultUser = Data.define(:login, :name) do
+  SentryConfig = Data.define(
+    :dsn, #: String
+    :env, #: String?
+  )
+
+  DefaultUser = Data.define(
+    :login, #: String
+    :name, #: String
+  )
+
+  # Reopened because rbs-inline ignores methods defined inside Data.define blocks.
+  class DefaultUser
+    #: (String) -> DefaultUser
     def self.parse(value)
-      login, name = value.split(":", 2)
+      login, name = value.split(":", 2) #: [String, String?]
       new(login: login, name: name || login)
     end
   end
 
+  #: () -> String
   def to_s
     parts = ["database=#{database_url}"]
     parts << "sentry=#{sentry.env || "on"}" if sentry
@@ -21,6 +41,7 @@ class Config
     "Config(#{parts.join(", ")})"
   end
 
+  #: (?Hash[String, String] env) -> Config
   def self.from_env(env = ENV)
     sentry_dsn = env["SENTRY_DSN"]
     default_user = env["DEFAULT_USER"]