Add RBS type signatures to all lib/ source files
Adds # rbs_inline: enabled pragmas and type annotations to all Ruby
source files in lib/. Generates corresponding RBS signatures via
rbs-inline and adds a sig/shim.rbs for third-party gem types (Sequel,
Roda, Phlex) that can't be fetched via rbs collection in this
environment. Expands Steepfile to check all typed files. Excludes
lib/web.rb from Steep checking because Roda's route-block self-rewriting
is incompatible with static analysis.

Assisted-by: Owl Alpha via pi
change mqpyxlvnzpkxosqnpzvqzkrxmlyspumv
commit e89d901e370998fed692dc3651c867eab1337d10
author Alpha Chen <alpha@kejadlen.dev>
date
parent b6645cce
diff --git a/Steepfile b/Steepfile
index ab13d20..8e7d2a2 100644
--- a/Steepfile
+++ b/Steepfile
@@ -1,4 +1,11 @@
 target :lib do
   signature "sig"
   check "lib/config.rb"
+  check "lib/app.rb"
+  check "lib/models.rb"
+  check "lib/relative_time.rb"
+  check "lib/views/layout.rb"
+  check "lib/views/home.rb"
+  check "lib/views/capture_form.rb"
+  check "lib/views/icons.rb"
 end
diff --git a/lib/app.rb b/lib/app.rb
index f72658a..389ccaa 100644
--- a/lib/app.rb
+++ b/lib/app.rb
@@ -1,3 +1,4 @@
+# rbs_inline: enabled
 # frozen_string_literal: true
 
 require "sequel"
@@ -7,11 +8,13 @@ module Domus
   class App
     attr_reader :config, :db
 
+    # : (Config) -> void
     def initialize(config = Config.env)
       @config = config
       @db = Sequel.sqlite(config.database_url)
     end
 
+    # : (Hash[Symbol, untyped]) -> Pathname
     def file_path(record)
       config.storage_path / "files" / "#{record[:id]}#{record[:extension]}"
     end
diff --git a/lib/models.rb b/lib/models.rb
index dff4000..dd524ca 100644
--- a/lib/models.rb
+++ b/lib/models.rb
@@ -1,3 +1,4 @@
+# rbs_inline: enabled
 # frozen_string_literal: true
 
 module Domus
diff --git a/lib/relative_time.rb b/lib/relative_time.rb
index e169732..a7ac6d1 100644
--- a/lib/relative_time.rb
+++ b/lib/relative_time.rb
@@ -1,3 +1,4 @@
+# rbs_inline: enabled
 # frozen_string_literal: true
 
 module Domus
@@ -6,6 +7,7 @@ module Domus
   module RelativeTime
     module_function
 
+    # : (Time at, ?now: Time) -> String
     def format(at, now: Time.now)
       return "" unless at
 
diff --git a/lib/views/capture_form.rb b/lib/views/capture_form.rb
index 6cf14c3..e570b19 100644
--- a/lib/views/capture_form.rb
+++ b/lib/views/capture_form.rb
@@ -1,3 +1,4 @@
+# rbs_inline: enabled
 # frozen_string_literal: true
 
 require "phlex"
diff --git a/lib/views/home.rb b/lib/views/home.rb
index c1278a6..5bd6252 100644
--- a/lib/views/home.rb
+++ b/lib/views/home.rb
@@ -1,3 +1,4 @@
+# rbs_inline: enabled
 # frozen_string_literal: true
 
 require "phlex"
@@ -15,6 +16,7 @@ module Domus
     class Home < Phlex::HTML
       include Icons
 
+      # : (assets: Array[Hash[Symbol, untyped]], total: Integer) -> void
       def initialize(assets:, total:)
         @assets = assets
         @total = total
diff --git a/lib/views/icons.rb b/lib/views/icons.rb
index 7c108e3..2f55d5f 100644
--- a/lib/views/icons.rb
+++ b/lib/views/icons.rb
@@ -1,3 +1,4 @@
+# rbs_inline: enabled
 # frozen_string_literal: true
 
 require "pathname"
@@ -8,12 +9,13 @@ module Domus
     # and can be sized and recoloured with CSS. Mixed into the Phlex views
     # that draw icons.
     module Icons
-      ICONS_DIR = (Pathname(__dir__) / "../../public/icons").expand_path
+      ICONS_DIR = (Pathname(__dir__.to_s) / "../../public/icons").expand_path
 
       ICONS = Hash.new do |cache, name|
         cache[name] = (ICONS_DIR / "#{name}.svg").read.freeze
       end
 
+      # : (String name) -> void
       def icon(name)
         raw safe(ICONS[name])
       end
diff --git a/lib/views/layout.rb b/lib/views/layout.rb
index 33d19c9..e423156 100644
--- a/lib/views/layout.rb
+++ b/lib/views/layout.rb
@@ -1,3 +1,4 @@
+# rbs_inline: enabled
 # frozen_string_literal: true
 
 require "phlex"
@@ -10,6 +11,7 @@ module Domus
       # `scripts` are emitted (deferred) before Alpine so page scripts that
       # register Alpine components — e.g. /capture.js defining captureApp() —
       # run first. Order matters: deferred scripts execute in document order.
+      # : (?title: String, ?scripts: Array[String]) -> void
       def initialize(title: "Domus", scripts: [])
         @title = title
         @scripts = scripts
diff --git a/lib/web.rb b/lib/web.rb
index b2a580e..120a381 100644
--- a/lib/web.rb
+++ b/lib/web.rb
@@ -1,3 +1,4 @@
+# rbs_inline: enabled
 # frozen_string_literal: true
 
 require "roda"
@@ -11,6 +12,7 @@ module Domus
   class ClientError < StandardError
     attr_reader :status
 
+    # : (String message, ?status: Integer) -> void
     def initialize(message, status: 422)
       super(message)
       @status = status
@@ -56,11 +58,14 @@ module Domus
 
     private
 
+    # : () -> App
     def app = opts.fetch(:app)
+    # : () -> Sequel::Database
     def db = app.db
 
     # Persists an uploaded image, raising ClientError when the upload is
     # rejected so the error_handler plugin can render the right status.
+    # : (Hash[String, untyped]) -> void
     def save_file(params)
       upload = params["file"]
       raise ClientError, "Choose a file to upload." unless upload.is_a?(Hash) && upload[:tempfile]
diff --git a/sig/generated/app.rbs b/sig/generated/app.rbs
new file mode 100644
index 0000000..e3d8ddf
--- /dev/null
+++ b/sig/generated/app.rbs
@@ -0,0 +1,15 @@
+# Generated from lib/app.rb with RBS::Inline
+
+module Domus
+  class App
+    attr_reader config: untyped
+
+    attr_reader db: untyped
+
+    # : (Config) -> void
+    def initialize: (?untyped config) -> untyped
+
+    # : (Hash[Symbol, untyped]) -> Pathname
+    def file_path: (untyped record) -> untyped
+  end
+end
diff --git a/sig/generated/models.rbs b/sig/generated/models.rbs
new file mode 100644
index 0000000..627671e
--- /dev/null
+++ b/sig/generated/models.rbs
@@ -0,0 +1,6 @@
+# Generated from lib/models.rb with RBS::Inline
+
+module Domus
+  class Document < Sequel::Model
+  end
+end
diff --git a/sig/generated/relative_time.rbs b/sig/generated/relative_time.rbs
new file mode 100644
index 0000000..ba0d109
--- /dev/null
+++ b/sig/generated/relative_time.rbs
@@ -0,0 +1,10 @@
+# Generated from lib/relative_time.rb with RBS::Inline
+
+module Domus
+  # Formats a timestamp as a compact, archival relative age —
+  # "now", "5m", "3h", "2d", "1w", "4mo", "2y".
+  module RelativeTime
+    # : (Time at, ?now: Time) -> String
+    def self?.format: (untyped at, ?now: untyped) -> untyped
+  end
+end
diff --git a/sig/generated/views/capture_form.rbs b/sig/generated/views/capture_form.rbs
new file mode 100644
index 0000000..a21d78b
--- /dev/null
+++ b/sig/generated/views/capture_form.rbs
@@ -0,0 +1,15 @@
+# Generated from lib/views/capture_form.rb with RBS::Inline
+
+module Domus
+  module Views
+    # The save step of a capture: the hidden camera/file inputs plus the
+    # preview and asset-naming form. Driven by the captureApp() Alpine data,
+    # and shared by both the capture page and the home page so the capture
+    # actions can open the picker in place.
+    class CaptureForm < Phlex::HTML
+      include Icons
+
+      def view_template: () -> untyped
+    end
+  end
+end
diff --git a/sig/generated/views/home.rbs b/sig/generated/views/home.rbs
new file mode 100644
index 0000000..27a3548
--- /dev/null
+++ b/sig/generated/views/home.rbs
@@ -0,0 +1,42 @@
+# Generated from lib/views/home.rb with RBS::Inline
+
+module Domus
+  module Views
+    # The home page — the archive's front door. Capture actions live in a
+    # thumb-reachable dock and open the picker in place: the whole page is one
+    # captureApp() Alpine component, so choosing a file swaps the recent-asset
+    # list for the save form.
+    class Home < Phlex::HTML
+      include Icons
+
+      # : (assets: Array[Hash[Symbol, untyped]], total: Integer) -> void
+      def initialize: (assets: untyped, total: untyped) -> untyped
+
+      def view_template: () -> untyped
+
+      private
+
+      def topbar: () -> untyped
+
+      # A single split button: the primary action opens the picker
+      # captureApp() chooses for the viewport (upload on desktop, camera on
+      # mobile), and the caret opens a drop-up menu with the alternate.
+      # Both labels are rendered and toggled by media query.
+      def capture_control: () -> untyped
+
+      def recent_assets: () -> untyped
+
+      def archive: () -> untyped
+
+      def empty_state: () -> untyped
+
+      # The capture save step, shown once a photo or file has been chosen.
+      def save_panel: () -> untyped
+
+      # The capture front door: a dock fixed to the bottom of the viewport on
+      # every screen, so the primary action stays reachable and out of the
+      # header.
+      def dock: () -> untyped
+    end
+  end
+end
diff --git a/sig/generated/views/icons.rbs b/sig/generated/views/icons.rbs
new file mode 100644
index 0000000..9ed6ec2
--- /dev/null
+++ b/sig/generated/views/icons.rbs
@@ -0,0 +1,20 @@
+# Generated from lib/views/icons.rb with RBS::Inline
+
+module Domus
+  module Views
+    # Inlines the SVG files from public/icons so they inherit currentColor
+    # and can be sized and recoloured with CSS. Mixed into the Phlex views
+    # that draw icons.
+    module Icons
+      ICONS_DIR: untyped
+
+      ICONS: untyped
+
+      # : (String name) -> void
+      def icon: (untyped name) -> untyped
+
+      def raw: (String) -> void
+      def safe: (String) -> String
+    end
+  end
+end
diff --git a/sig/generated/views/layout.rbs b/sig/generated/views/layout.rbs
new file mode 100644
index 0000000..45cef45
--- /dev/null
+++ b/sig/generated/views/layout.rbs
@@ -0,0 +1,17 @@
+# Generated from lib/views/layout.rb with RBS::Inline
+
+module Domus
+  module Views
+    class Layout < Phlex::HTML
+      ALPINE: ::String
+
+      # `scripts` are emitted (deferred) before Alpine so page scripts that
+      # register Alpine components — e.g. /capture.js defining captureApp() —
+      # run first. Order matters: deferred scripts execute in document order.
+      # : (?title: String, ?scripts: Array[String]) -> void
+      def initialize: (?title: untyped, ?scripts: untyped) -> untyped
+
+      def view_template: () ?{ (?) -> untyped } -> untyped
+    end
+  end
+end
diff --git a/sig/generated/web.rbs b/sig/generated/web.rbs
new file mode 100644
index 0000000..288bf4b
--- /dev/null
+++ b/sig/generated/web.rbs
@@ -0,0 +1,31 @@
+# Generated from lib/web.rb with RBS::Inline
+
+module Domus
+  # Raised when a request can't be processed because of client input. The
+  # error_handler plugin renders it with the HTTP status it carries.
+  class ClientError < StandardError
+    attr_reader status: untyped
+
+    # : (String message, ?status: Integer) -> void
+    def initialize: (untyped message, ?status: untyped) -> untyped
+  end
+
+  class Web < Roda
+    IMAGE_EXTENSIONS: untyped
+
+    MAX_UPLOAD_BYTES: untyped
+
+    private
+
+    # : () -> App
+    def app: () -> untyped
+
+    # : () -> Sequel::Database
+    def db: () -> untyped
+
+    # Persists an uploaded image, raising ClientError when the upload is
+    # rejected so the error_handler plugin can render the right status.
+    # : (Hash[String, untyped]) -> void
+    def save_file: (untyped params) -> untyped
+  end
+end
diff --git a/sig/shim.rbs b/sig/shim.rbs
new file mode 100644
index 0000000..55bf4ad
--- /dev/null
+++ b/sig/shim.rbs
@@ -0,0 +1,74 @@
+# Shim declarations for gems without RBS in this environment.
+# Covers only the methods actually used in the Domus codebase.
+
+module Sequel
+  def self.sqlite: (String) -> Sequel::Database
+  def self.desc: (Symbol) -> untyped
+
+  class Database
+    def []: (Symbol) -> Sequel::Dataset
+    def transaction: () { () -> void } -> void
+  end
+
+  class Dataset
+    def order: (*untyped) -> Sequel::Dataset
+    def limit: (Integer) -> Sequel::Dataset
+    def all: () -> Array[Hash[Symbol, untyped]]
+    def count: () -> Integer
+    def insert: (Hash[Symbol, untyped]) -> Integer
+  end
+
+  class Model
+  end
+end
+
+class Roda
+  # Class-level DSL
+  def self.plugin: (Symbol) ?{ (Class) -> void } -> void
+  def self.route: () { (Roda) -> void } -> void
+
+  # Instance-level DSL (used inside route blocks)
+  def db: () -> Sequel::Database
+  def response: () -> untyped
+  def opts: () -> Hash[Symbol, untyped]
+end
+
+module Phlex
+  class HTML
+    def self.new: (**untyped) -> instance
+
+    def view_template: () { () -> void } -> void
+    def render: (untyped) ?{ () -> void } -> void
+    def call: () -> String
+
+    # HTML elements used in views
+    def doctype: () -> void
+    def html: (?lang: String) { () -> void } -> void
+    def head: () { () -> void } -> void
+    def body: () ?{ () -> void } -> void
+    def meta: (?charset: String, ?name: String, ?content: String) -> void
+    def title: () { () -> void } -> void
+    def link: (?rel: String, ?type: String, ?href: String) -> void
+    def script: (?defer: bool, ?src: String) -> void
+    def div: (?class: String, **untyped) { () -> void } -> void
+    def span: (?class: String, **untyped) { () -> void } -> void
+    def a: (?href: String, ?class: String, **untyped) { () -> void } -> void
+    def h3: (**untyped) { () -> void } -> void
+    def p: (?class: String, **untyped) { () -> void } -> void
+    def section: (**untyped) { () -> void } -> void
+    def header: (?class: String, **untyped) { () -> void } -> void
+    def main: (?class: String, **untyped) { () -> void } -> void
+    def form: (?method: String, ?action: String, ?enctype: String, **untyped) { () -> void } -> void
+    def input: (?type: String, ?name: String, ?accept: String, ?capture: String, ?class: String, ?placeholder: String, **untyped) -> void
+    def button: (?type: String, ?class: String, **untyped) { () -> void } -> void
+    def img: (?alt: String, **untyped) -> void
+    def template: (**untyped) { () -> void } -> void
+
+    # Text output
+    def plain: (String) -> void
+
+    # Raw HTML
+    def raw: (String) -> void
+    def safe: (String) -> String
+  end
+end