diff --git a/.rubocop.yml b/.rubocop.yml index 31021238..f3765127 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -42,7 +42,7 @@ Style/ItBlockParameter: EnforcedStyle: allow_single_line Style/EmptyLiteral: - Enabled: true + Enabled: false Style/BlockDelimiters: Enabled: true @@ -50,7 +50,6 @@ Style/BlockDelimiters: Style/TrivialAccessors: Enabled: true - Layout/LineLength: Enabled: true Max: 140 diff --git a/AGENTS.md b/AGENTS.md index 526f9959..bdf49df3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -97,11 +97,68 @@ This is a Rails API app for the `r3x` Ruby-native workflow engine. Keep changes ## Naming, Zeitwerk, Autoloading -- Do not repeat namespace names in class names: `R3x::Client::Http`, not `R3x::Client::HttpClient`. -- Follow Zeitwerk path-to-constant mapping exactly. Files, directories, namespaces, and acronyms must align. -- Do not `require` or `require_relative` files from Rails autoload paths. Just reference the constant. Require only external gems/components that do not auto-require. -- One class per file. Extract nested classes to files under the parent namespace. - +This repo uses `.githooks/` directory for git hooks. The pre-commit hook runs `bin/ci` which includes `bin/lint-r3x` to verify AGENTS.md references. + +## Static Typing + +- This repo uses RBS + Steep for static type checks. The current Steep scope is intentionally narrow and starts with the workflow-facing API contract. +- Run `bin/typecheck` after changing files covered by `Steepfile`. +- When changing `R3x::Workflow::Context`, `R3x::Workflow::Context::Client`, or `R3x::Client::Http`, update the matching files under `sig/`. +- When adding a new workflow client method to `R3x::Workflow::Context::Client`, add its RBS signature and any minimal dependency stub needed for `bin/typecheck`. +- Keep the Steep scope narrow. Do not expand it to the whole Rails app unless explicitly planned. +- Treat `sig/r3x/client/_stubs.rbs` and `sig/r3x/external_stubs.rbs` as dependency stubs, not source of truth for unchecked implementation details. + +## Iterative Design Reviews + +- When the user is still reviewing the shape of a refactor or API design, keep the first pass focused on production code only unless they explicitly ask for full follow-through. +- Do not update tests, fixtures, or RBS signatures for an unaccepted design sketch. Wait until the user accepts the code shape, then synchronize tests and `sig/` files in the follow-up pass. +- It is still fine to run syntax checks on the touched Ruby files during the sketch phase. Do not treat typecheck failures caused only by intentionally stale RBS as blockers until the user accepts the design. + +## JSON + +- Prefer `MultiJSON` for JSON parsing and serialization work. +- Reasoning: it gives the app one consistent JSON abstraction instead of scattering direct `JSON` stdlib usage across the codebase, which makes adapter swaps and shared conventions easier later. + +## HTTP + +- Prefer `httpx` for outbound HTTP and API integrations in `R3x::Client` code. +- Reasoning: `httpx` provides native multipart uploads, JSON request/response handling, persistent connections, and built-in retries without the middleware boilerplate that Faraday requires. It reduces client code volume and eliminates thread-unsafe runtime middleware mutation. +- `Faraday` remains in `Gemfile.lock` as a transitive dependency of `googleauth`, `ruby_llm`, and `google-apis-*`. Do not add new direct Faraday dependencies or use Faraday in internal clients. +- For small integration clients under `R3x::Client`, build the `httpx` client inside the class instead of injecting a `connection` dependency. +- Reasoning: these clients are thin integration boundaries, so passing a raw HTTP client through the initializer adds indirection without improving the public interface we actually want to use. +- Do not use empty `HTTPX.with({})`; call `HTTPX.get/post/...` directly when there are no shared options. When multiple code paths need the same timeout/SSL/options payload for `HTTPX.with`, build that payload once in a small helper and pass it with keyword splat. Use Ruby keyword argument shorthand (`verify_ssl:, timeout:`) when the local variable names match the keyword names. In options helpers, set only the timeout by default and conditionally assign the `ssl` key (e.g. `opts[:ssl] = ... unless verify_ssl`) so the `ssl` option is entirely omitted when SSL verification is enabled. +- For workflow code that needs repeated HTTP requests in one controlled scope, prefer the block-scoped `ctx.client.persistent_http(...) { |http| ... }` helper over exposing raw `HTTPX.plugin(:persistent)` usage in workflows. Keep persistence opt-in for measured batch/hot paths, not as a default for thin clients. +- **JSON handling**: When making HTTP requests that send/receive JSON, use `httpx`'s native `json:` option instead of manually serializing with `MultiJSON`. This automatically sets the `Content-Type` header and handles serialization. + - **Bad**: `request.body = MultiJSON.generate({"key" => "value"})` + - **Good**: `client.post(url, json: { key: "value" })` +- **Error handling**: `httpx` does not raise on 4xx/5xx by default. Call `.raise_for_status` on the response when the client should fail fast on HTTP errors, matching the previous `Faraday::Error` behavior. Do not hand-roll ordinary HTTP status checks in thin `R3x::Client` wrappers. + - **Bad**: `response = client.get(url)` (silently ignores 404/500) + - **Bad**: `raise "Request failed: #{response.status}" unless response.status >= 200 && response.status < 300` + - **Good**: `response = client.post(url, json: payload).raise_for_status` + - Manual error translation is still fine for provider-specific response bodies after the transport status has succeeded, e.g. an API returns `200` with `{ "IsErroredOnProcessing": true }`. + +## Naming Conventions + +- When a class is namespaced within a descriptive module (e.g., `R3x::Client`, `R3x::Triggers`), do not repeat the module name in the class name. +- **Good**: `R3x::Client::Http`, `R3x::Triggers::Schedule`, `R3x::Client::Discord::Webhook` +- **Bad**: `R3x::Client::HttpClient`, `R3x::Triggers::ScheduleTrigger` + +### Zeitwerk & File Structure + +- Adhere strictly to Zeitwerk's path-to-constant mapping: file names must match their defined constant exactly (snake_case to CamelCase). +- **Files**: `app/lib/r3x/client/http.rb` must define `R3x::Client::Http`. +- **Directories**: Directories represent namespaces. If a file is in `app/models/r3x/`, it must be wrapped in `module R3x`. +- **Acronyms**: Use standard inflection (e.g., `lib/r3x/env.rb` → `R3x::Env`) unless a custom inflection is explicitly defined in `config/initializers/inflections.rb`. +- **Validation**: Always ensure the filename and the class/module name are perfectly aligned to avoid `NameError` during autoloading. +- **One class per file**: Extract nested classes to files under the parent namespace directory structure. + +### Autoloading + +- Everything autoloaded by Rails (paths configured in `autoload_paths`, `autoload_lib`, etc.) is handled by Zeitwerk. You should never need to use `require` or `require_relative` for files within autoloaded paths. +- **Bad**: `require_relative "../validators/cron"` at the top of a file in `lib/r3x/triggers/` +- **Good**: Just reference `R3x::Validators::Cron` directly - Zeitwerk will find and load it automatically. +- The only exception is requiring external gems that don't auto-require their components. +- **Debugging**: If you get a `NameError` when referencing a class that should exist, it's likely a Zeitwerk autoloading issue (wrong file name, wrong constant name, or missing namespace). Check that file names match constants exactly (snake_case ↔ CamelCase). ## Object Design - Prefer capability-based APIs over branching on symbolic types when behavior can live on the object. diff --git a/Gemfile b/Gemfile index 3efc3cce..2dc4d017 100644 --- a/Gemfile +++ b/Gemfile @@ -72,6 +72,10 @@ group :development, :test do gem "rubocop-minitest", require: false gem "rubocop-thread_safety", "~> 0.7.3", require: false + # Narrow static type-checking spike for workflow-facing API contracts. + gem "rbs", require: false + gem "steep", require: false + # Auto-load environment variables from .env file gem "dotenv-rails", require: false end diff --git a/Gemfile.lock b/Gemfile.lock index 023f21b0..817efbc9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -95,6 +95,7 @@ GEM bigdecimal rexml crass (1.0.6) + csv (3.3.5) date (3.5.1) debug (1.11.1) irb (~> 1.10) @@ -122,6 +123,15 @@ GEM net-http (~> 0.5) faraday-retry (2.4.0) faraday (~> 2.0) + ffi (1.17.4-aarch64-linux-gnu) + ffi (1.17.4-aarch64-linux-musl) + ffi (1.17.4-arm-linux-gnu) + ffi (1.17.4-arm-linux-musl) + ffi (1.17.4-arm64-darwin) + ffi (1.17.4-x86_64-darwin) + ffi (1.17.4-x86_64-linux-gnu) + ffi (1.17.4-x86_64-linux-musl) + fileutils (1.8.0) fugit (1.12.2) et-orbi (~> 1.4) raabro (~> 1.4) @@ -267,6 +277,10 @@ GEM base64 language_server-protocol (3.17.0.5) lint_roller (1.1.0) + listen (3.10.0) + logger + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) logger (1.7.0) loofah (2.25.1) crass (~> 1.0.2) @@ -394,6 +408,13 @@ GEM zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.4.2) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) + ffi (~> 1.0) + rbs (4.0.2) + logger + prism (>= 1.6.0) + tsort rdoc (7.2.0) erb psych (>= 4.0.0) @@ -483,9 +504,28 @@ GEM sqlite3 (2.9.5-x86_64-darwin) sqlite3 (2.9.5-x86_64-linux-gnu) sqlite3 (2.9.5-x86_64-linux-musl) + steep (2.0.0) + concurrent-ruby (>= 1.1.10) + csv (>= 3.0.9) + fileutils (>= 1.1.0) + json (>= 2.1.0) + language_server-protocol (>= 3.17.0.4, < 4.0) + listen (~> 3.0) + logger (>= 1.3.0) + parser (>= 3.2) + prism (>= 0.25.0) + rainbow (>= 2.2.2, < 4.0) + rbs (~> 4.0) + securerandom (>= 0.1) + strscan (>= 1.0.0) + terminal-table (>= 2, < 5) + uri (>= 0.12.0) stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.2.0) + strscan (3.1.8) + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) thor (1.5.0) timeout (0.6.1) trailblazer-option (0.1.2) @@ -545,6 +585,7 @@ DEPENDENCIES propshaft puma (>= 5.0) rails (~> 8.1.2) + rbs retryable (~> 3.0) rss rubocop-minitest @@ -555,6 +596,7 @@ DEPENDENCIES solid_cache solid_queue sqlite3 (>= 2.1) + steep webmock CHECKSUMS @@ -583,6 +625,7 @@ CHECKSUMS connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a crack (1.0.1) sha256=ff4a10390cd31d66440b7524eb1841874db86201d5b70032028553130b6d4c7e crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d + csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6 declarative (0.0.20) sha256=8021dd6cb17ab2b61233c56903d3f5a259c5cf43c80ff332d447d395b17d9ff9 @@ -598,6 +641,15 @@ CHECKSUMS faraday-multipart (1.2.0) sha256=7d89a949693714176f612323ca13746a2ded204031a6ba528adee788694ef757 faraday-net_http (3.4.4) sha256=0e78af151747ed1b00f33e25973b4bc220d7f16c00c39676817c8b12331eb588 faraday-retry (2.4.0) sha256=7b79c48fb7e56526faf247b12d94a680071ff40c9fda7cf1ec1549439ad11ebe + ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df + ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39 + ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564 + ffi (1.17.4-arm-linux-musl) sha256=9d4838ded0465bef6e2426935f6bcc93134b6616785a84ffd2a3d82bc3cf6f95 + ffi (1.17.4-arm64-darwin) sha256=19071aaf1419251b0a46852abf960e77330a3b334d13a4ab51d58b31a937001b + ffi (1.17.4-x86_64-darwin) sha256=aa70390523cf3235096cf64962b709b4cfbd5c082a2cb2ae714eb0fe2ccda496 + ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d + ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e + fileutils (1.8.0) sha256=8c6b1df54e2540bdb2f39258f08af78853aa70bad52b4d394bbc6424593c6e02 fugit (1.12.2) sha256=643f2bf28db263bd400cbf8e0dd8b76b2c9b94bdb130e12d2394de04d9c20e5e gapic-common (1.3.0) sha256=4e5d9be1effb7366e2cda13c0f44922285d5613ac8de8b9b18c2c835fe4faa91 globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11 @@ -645,6 +697,7 @@ CHECKSUMS jwt (3.2.0) sha256=5419b1fe37b1da0982bd07051f573a8b8789ab724c2aa7e785e4784a3ed217d7 language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 + listen (3.10.0) sha256=c6e182db62143aeccc2e1960033bebe7445309c7272061979bb098d03760c9d2 logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 loofah (2.25.1) sha256=d436c73dbd0c1147b16c4a41db097942d217303e1f7728704b37e4df9f6d2e04 mail (2.9.0) sha256=6fa6673ecd71c60c2d996260f9ee3dd387d4673b8169b502134659ece6d34941 @@ -700,6 +753,9 @@ CHECKSUMS railties (8.1.3) sha256=913eb0e0cb520aac687ffd74916bd726d48fa21f47833c6292576ef6a286de22 rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + rb-fsevent (0.11.2) sha256=43900b972e7301d6570f64b850a5aa67833ee7d87b458ee92805d56b7318aefe + rb-inotify (0.11.1) sha256=a0a700441239b0ff18eb65e3866236cd78613d6b9f78fea1f9ac47a85e47be6e + rbs (4.0.2) sha256=af75671e66cd03434cc546622741ebf83f6197ec4328375805306330bf78ef25 rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192 regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835 @@ -731,8 +787,11 @@ CHECKSUMS sqlite3 (2.9.5-x86_64-darwin) sha256=8e9caae38bd7ebb29cbeee3e7ab1d12dc2327d9a1b92c7fcf0dda05589627a81 sqlite3 (2.9.5-x86_64-linux-gnu) sha256=233dbcb6714148dd23bc5aeb33e8efd6eac974969564ddd5794c23d5f52b231e sqlite3 (2.9.5-x86_64-linux-musl) sha256=e7d3a7474e8af0f96150c21abc203fbab5437206bfcdf11deab7741c0ca516f2 + steep (2.0.0) sha256=6eb0ecc09637bbb54f0a5f2cf63daea6d3208ccace64b4f1107d976333605c30 stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06 stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 + strscan (3.1.8) sha256=aae2db611a225559f21ffbb71765c9a4e60fd262534a9ea84f4f11c7f32f679e + terminal-table (4.0.0) sha256=f504793203f8251b2ea7c7068333053f0beeea26093ec9962e62ea79f94301d2 thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb trailblazer-option (0.1.2) sha256=20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3 diff --git a/Steepfile b/Steepfile new file mode 100644 index 00000000..dc5211b2 --- /dev/null +++ b/Steepfile @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +target :static_types do + signature "sig" + + check "lib/r3x/workflow/context.rb" + check "lib/r3x/workflow/context/client.rb" + + check "app/lib/r3x/client/http.rb" + check "app/lib/r3x/client/http/downloaded_file.rb" + check "app/lib/r3x/client/miniflux.rb" + check "app/lib/r3x/client/victoria_logs.rb" + + check "app/lib/r3x/client/apify.rb" + check "app/lib/r3x/client/discord.rb" + check "app/lib/r3x/client/google/gmail.rb" + check "app/lib/r3x/client/google/translate.rb" + check "app/lib/r3x/client/google_sheets.rb" + check "app/lib/r3x/client/healthchecks_io.rb" + check "app/lib/r3x/client/llm.rb" + check "app/lib/r3x/client/markdownify.rb" + check "app/lib/r3x/client/ocr.rb" + check "app/lib/r3x/client/prometheus.rb" +end diff --git a/app/lib/r3x/client/google/gmail.rb b/app/lib/r3x/client/google/gmail.rb index 50bfffc2..9af6c241 100644 --- a/app/lib/r3x/client/google/gmail.rb +++ b/app/lib/r3x/client/google/gmail.rb @@ -47,7 +47,7 @@ def raw_message(to:, subject:, body:, attachments: []) mail.subject = subject if attachments.any? - mail.text_part = Mail::Part.new { it.body = body } + mail.text_part = Mail::Part.new { |part| part.body = body } attachments.each { |attachment| mail.add_file(filename: attachment[:filename], content: attachment[:content]) } else diff --git a/app/lib/r3x/client/http.rb b/app/lib/r3x/client/http.rb index 832e1474..a55202ce 100644 --- a/app/lib/r3x/client/http.rb +++ b/app/lib/r3x/client/http.rb @@ -20,7 +20,7 @@ def with_persistence(verify_ssl: true, timeout: nil) private def httpx_options(verify_ssl:, timeout:) - opts = {} + opts = Hash.new opts[:timeout] = { operation_timeout: timeout } if timeout opts[:ssl] = { verify_mode: OpenSSL::SSL::VERIFY_NONE } unless verify_ssl opts diff --git a/app/lib/r3x/client/llm.rb b/app/lib/r3x/client/llm.rb index d6efa2e6..ef748a6c 100644 --- a/app/lib/r3x/client/llm.rb +++ b/app/lib/r3x/client/llm.rb @@ -6,8 +6,8 @@ class Llm MAX_RETRIES = 3 RETRY_INTERVAL = 60.0 RETRY_BACKOFF_FACTOR = 2 - DEFAULT_CHAT_OPTIONS = {}.freeze - CHAT_OPTIONS_BY_PROVIDER = {} # rubocop:disable Style/MutableConstant + DEFAULT_CHAT_OPTIONS = Hash.new.freeze + CHAT_OPTIONS_BY_PROVIDER = Hash.new CHAT_OPTIONS_BY_PROVIDER_MUTEX = Mutex.new class << self diff --git a/app/lib/r3x/client/ocr.rb b/app/lib/r3x/client/ocr.rb index 1a67d3c7..3680cc46 100644 --- a/app/lib/r3x/client/ocr.rb +++ b/app/lib/r3x/client/ocr.rb @@ -56,7 +56,7 @@ def detect_mime(io_or_path) end def build_params(io_or_path, mime_type, language:, engine:, overlay:) - params = {} + params = Hash.new params[:isOverlayRequired] = overlay.to_s params[:language] = language if language params[:OCREngine] = engine.to_s if engine diff --git a/app/lib/r3x/client/victoria_logs.rb b/app/lib/r3x/client/victoria_logs.rb index 4bc9062b..5642b036 100644 --- a/app/lib/r3x/client/victoria_logs.rb +++ b/app/lib/r3x/client/victoria_logs.rb @@ -7,7 +7,7 @@ class VictoriaLogs DEFAULT_TIMEOUT = "5s".freeze def self.configured? - R3x::Env.fetch(DEFAULT_URL_ENV).present? + R3x::Env.present?(DEFAULT_URL_ENV) end def initialize(url_env: DEFAULT_URL_ENV) diff --git a/bin/typecheck b/bin/typecheck new file mode 100755 index 00000000..32c3cefc --- /dev/null +++ b/bin/typecheck @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" + +exec Gem.bin_path("steep", "steep"), "check", *ARGV diff --git a/config/ci.rb b/config/ci.rb index d1d036b9..0950c07c 100644 --- a/config/ci.rb +++ b/config/ci.rb @@ -8,6 +8,7 @@ step "Style: Ruby", "bin/rubocop" step "Style: YAML", "bin/dprint check" step "Lint: AGENTS.md references", "bin/lint-r3x" + step "Types: Static Types", "bin/typecheck" step "Security: Gem audit", "bin/bundler-audit" step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error" diff --git a/docs/todo.md b/docs/todo.md index 40c855d7..97d80a3f 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -53,6 +53,7 @@ live at the Solid Queue boundary. - `app/lib/r3x/client/healthchecks_io.rb` - `app/lib/r3x/client/markdownify.rb` - `app/lib/r3x/client/victoria_logs.rb` +- `lib/r3x/workflow/context/client.rb` Several clients built `HTTPX.with(...)` timeout options by hand even when the provider did not need custom timeout policy. @@ -62,6 +63,8 @@ use HTTPX defaults. `R3x::Client::Http` now keeps its own option builder private and only applies options when callers request custom SSL or timeout behavior. Client-specific headers stay near the request code. +Also resolved an issue where `Context::Client.llm` explicitly forwarded default keyword arguments (e.g. `max_retries: nil`), overriding the constructor-level defaults in `Llm.new` and disabling LLM retries. Refactored the method to use anonymous keyword arguments forwarding (`**`), ensuring default settings are correctly restored when not overridden by the workflow. + --- ### [ ] C. Normalize dry-run logging in integration clients @@ -147,25 +150,97 @@ scopes, status filtering, argument normalization, and count SQL remain on ## Phase 4 - Local Workflow +### [ ] I. Remove magic predicates in `TriggerManager::Execution` + +**File:** `lib/r3x/trigger_manager/execution.rb:16-26` + +Metaprogramming makes static analysis hard and can answer `true` to any `foo?` question. + +**Suggested fix:** replace with explicit predicates (`schedule?`, `manual?`) or a capability-based API. + +--- + +### [ ] J. `R3x::Workflow::CacheKey` relies on `RubyVM::InstructionSequence` + +**File:** `lib/r3x/workflow/cache_key.rb:28` + +```ruby +RubyVM::InstructionSequence.of(block).to_a.dig(4, :code_location) +``` + +This is brittle against interpreter changes and hard to understand. + +**Suggested fix:** use an explicit, stable cache key source (e.g. caller location + workflow key + explicit discriminator) instead of bytecode introspection. + +--- + +## Phase 4 — long-term / optional + +### [x] K. RBS signatures are stale for new clients + +**Files:** `sig/r3x/client/*.rbs`, `Steepfile` + +**Done:** + +- Added per-client RBS signatures under `sig/r3x/client/` for: + - `apify`, `discord`, `google/gmail`, `google/translate`, `google_sheets`, `healthchecks_io`, `markdownify`, `ocr`, `prometheus`, `llm` (and nested `Classifier`, `ProviderConfiguration`, `ProviderRegistry`). +- Added signatures for client result/response objects: `Ocr::Result`, `HealthchecksIO::Response`, `Prometheus::Result`. +- Removed the stale `sig/r3x/workflow/base.rbs` (the class was not in `Steepfile` and the signature was incomplete). +- Removed the consolidated `sig/r3x/client/_stubs.rbs` in favor of explicit per-client signature files. +- Added the new clients to `Steepfile` so `bin/typecheck` covers them. +- Added missing dependency stubs to `sig/r3x/external_stubs.rbs` (`HTTPX.get/post/head`, `R3x::Env.fetch!`, `R3x::Client::GoogleAuth`, `Google::Apis::*`, `Mail::Part`, `RubyLLM`, `SecureRandom`, `Base64`, `String#blank?`). +- Made small code tweaks to keep type-checking clean: + - `Ocr#build_params` now starts with an empty `Hash.new`. + - `Llm` uses `Hash.new` for class-level caches and an explicit block param instead of `it`. + - `Google::Translate#translate` validates the translations array explicitly. + - `Google::Gmail#raw_message` uses an explicit `Mail::Part` block param. + +**Verification:** `bin/typecheck` passes; `bin/rails test` passes; `bin/lint-r3x` passes. + +--- + ### [ ] F. Decide whether the pre-commit hook should keep running full CI **File:** `.githooks/pre-commit` -The hook currently runs `bin/ci`, which includes setup, RuboCop, dprint, -`bin/lint-r3x`, bundler-audit, Brakeman, and the full Rails test suite. This -makes every commit green but slows down local commit shaping. +The hook currently runs `bin/ci`, which includes setup, RuboCop, dprint, `bin/lint-r3x`, typecheck, bundler-audit, Brakeman, and the full Rails test suite. This makes every commit green but slows down local commit shaping. **Suggested fix:** - - Keep as-is if green commits matter more than fast local history editing. -- Otherwise move full `bin/ci` to pre-push or CI, and keep pre-commit focused on - formatting plus cheap lint checks. +- Otherwise move full `bin/ci` to pre-push or CI, and keep pre-commit focused on formatting plus cheap lint checks. + +--- + +## Architectural notes — what would DHH and Sandi Metz say? + +These are not actionable todos yet; they are framing notes for larger refactor discussions. + +### DHH / 37signals perspective + +- **Less layering, more Rails.** Much of `app/lib/r3x/dashboard/workflow/` (`Catalog`, `Summaries`, `Runs`) is logic that belongs in models under `app/models/dashboard/`, not in parallel service/query layers. `Dashboard::Run` is the model — let it own its queries and summaries. +- **Pre-commit running full CI is expensive.** Running `bin/ci` (RuboCop, dprint, typecheck, bundler-audit, brakeman, full test suite) on every commit slows the local loop. A lighter pre-commit with full CI on push is more Rails-like. +- **RBS/Steep may be overhead at this size.** Keeping signatures in sync is a tax. If the team is not actively relying on Steep, the narrow scope is good, but widening it would be a mistake. +- **Fail fast, don't swallow errors.** `R3x::Env.load_from_vault` catching `StandardError` violates the 37signals preference for loud failures over silent degradation. +- **Use your own conventions everywhere.** Direct `ENV.fetch` calls in `config/puma.rb` and `production.rb` undermine the `R3x::Env` helper and create inconsistent semantics. + +### Sandi Metz perspective + +- **Classes are too large.** `Dashboard::Run` (254 lines), `Summaries` (274 lines), and `ApplicationHelper` (378 lines) violate SRP. Extract `StatusResolver`, `ArgumentsNormalizer`, and `ErrorParser`. +- **Avoid magic predicates.** `R3x::TriggerManager::Execution` uses `method_missing` for trigger type predicates. Replace with explicit methods (`schedule?`, `manual?`) or a capability-based API. +- **Don't use bytecode introspection.** `R3x::Workflow::CacheKey` relies on `RubyVM::InstructionSequence.of(block)`. It will break on interpreter changes and is hard to reason about. Use an explicit, stable key. +- **Don't stub what you don't own.** `test/lib/r3x/client/llm_test.rb` stubs internal `RubyLLM` objects. Wrap LLM behind a narrow adapter and test your own boundary. +- **Duplicate HTTP setup signals a missing object.** Every client builds `HTTPX.with(...)` by hand. A shared `R3x::Client::HttpBuilder` (or extension of `R3x::Client::Http`) would remove copy-paste and make SSL/timeout policy consistent. + +### Top 3 rewrites worth considering + +1. **Split `Dashboard::Run` and `Summaries` into smaller, single-responsibility classes.** This is the highest-impact structural improvement. +2. **Introduce a shared HTTP builder and unify dry-run logging across clients.** Removes duplication and makes new integrations cheaper. +3. **Replace `method_missing` trigger predicates and bytecode-based cache keys with explicit implementations.** Eliminates two sources of fragility. --- ## Notes -- Prefer direct fixes over new cops or enforcement machinery unless the same - drift repeats. +- Prefer direct fixes over new cops or enforcement machinery unless the same drift repeats. - Keep behavior changes and mechanical/style cleanup in separate commits. - Before closing any item, run focused tests first, then `bin/ci`. diff --git a/sig/r3x/client/apify.rbs b/sig/r3x/client/apify.rbs new file mode 100644 index 00000000..f407dd93 --- /dev/null +++ b/sig/r3x/client/apify.rbs @@ -0,0 +1,24 @@ +module R3x + module Client + class Apify + include R3x::Concerns::Logger + + BASE_URL: String + DEFAULT_API_KEY_ENV: String + + def initialize: (api_key: String api_key) -> void + + def run_actor: (String actor_id, ?input: untyped input, **untyped options) -> Hash[untyped, untyped] + + def run_actor_sync_get_items: (String actor_id, ?input: untyped input, ?format: String format, ?clean: bool clean, ?limit: Integer? limit, **untyped options) -> untyped + + def raw: () -> untyped + + private + + attr_reader api_key: String + + def connection: () -> untyped + end + end +end diff --git a/sig/r3x/client/discord.rbs b/sig/r3x/client/discord.rbs new file mode 100644 index 00000000..e5dbde4f --- /dev/null +++ b/sig/r3x/client/discord.rbs @@ -0,0 +1,17 @@ +module R3x + module Client + class Discord + include R3x::Concerns::Logger + + DEFAULT_WEBHOOK_URL_ENV: String + + def initialize: (?webhook_url: String? webhook_url, ?webhook_url_env: String webhook_url_env) -> void + + def deliver: (content: String content) -> Hash[String, String] + + private + + attr_reader webhook_url: String + end + end +end diff --git a/sig/r3x/client/google/gmail.rbs b/sig/r3x/client/google/gmail.rbs new file mode 100644 index 00000000..ef96a89e --- /dev/null +++ b/sig/r3x/client/google/gmail.rbs @@ -0,0 +1,21 @@ +module R3x + module Client + module Google + class Gmail + include R3x::Concerns::Logger + + def initialize: (project: String project) -> void + + def deliver: (to: String to, subject: String subject, body: String body, ?attachments: Array[Hash[Symbol, untyped]] attachments) -> Hash[String, String] + + private + + attr_reader project: String + + def build_service: () -> untyped + + def raw_message: (to: String to, subject: String subject, body: String body, attachments: Array[Hash[Symbol, untyped]] attachments) -> String + end + end + end +end diff --git a/sig/r3x/client/google/translate.rbs b/sig/r3x/client/google/translate.rbs new file mode 100644 index 00000000..18d568b5 --- /dev/null +++ b/sig/r3x/client/google/translate.rbs @@ -0,0 +1,25 @@ +module R3x + module Client + module Google + class Translate + API_URL: String + + def initialize: (project: String project) -> void + + def translate: (untyped text, to: String to, ?from: String? from, ?format: String format) -> String + + private + + attr_reader project: String + + def authorization: () -> untyped + + def authorization_header: () -> Hash[String, String] + + def access_token: () -> String + + def request_body: (String text, to: String to, from: String? from, format: String format) -> Hash[Symbol, untyped] + end + end + end +end diff --git a/sig/r3x/client/google_sheets.rbs b/sig/r3x/client/google_sheets.rbs new file mode 100644 index 00000000..7d3be247 --- /dev/null +++ b/sig/r3x/client/google_sheets.rbs @@ -0,0 +1,21 @@ +module R3x + module Client + class GoogleSheets + def initialize: (spreadsheet_id: String spreadsheet_id, project: String project) -> void + + def read_rows: (range: String range, ?headers: bool headers) -> Array[untyped] + + private + + attr_reader spreadsheet_id: String + attr_reader project: String + attr_reader service: untyped + + def build_service: () -> untyped + + def make_unique_headers: (untyped headers) -> untyped + + def row_to_hash: (untyped headers, untyped row) -> Hash[untyped, untyped] + end + end +end diff --git a/sig/r3x/client/healthchecks_io.rbs b/sig/r3x/client/healthchecks_io.rbs new file mode 100644 index 00000000..97e3ca14 --- /dev/null +++ b/sig/r3x/client/healthchecks_io.rbs @@ -0,0 +1,29 @@ +module R3x + module Client + class HealthchecksIO + include R3x::Concerns::Logger + + def initialize: (String check_uuid, ?ping_endpoint: String? ping_endpoint, ?ping_endpoint_env: String ping_endpoint_env) -> void + + def run: () { (HealthchecksIO, String) -> untyped } -> untyped + + def ping: (?body: String? body, ?rid: String? rid) -> HealthchecksIO::Response + + def fail: (?body: String? body, ?rid: String? rid) -> HealthchecksIO::Response + + def log: (lines: Array[String] | String lines, ?rid: String? rid) -> HealthchecksIO::Response + + def exit_status: (code: Integer code, ?body: String? body, ?rid: String? rid) -> HealthchecksIO::Response + + private + + attr_reader ping_url: String + + def connection: () -> untyped + + def send_start: (?rid: String? rid) -> HealthchecksIO::Response + + def make_request: (Symbol method, String path, ?body: String? body, ?rid: String? rid) -> HealthchecksIO::Response + end + end +end diff --git a/sig/r3x/client/healthchecks_io/response.rbs b/sig/r3x/client/healthchecks_io/response.rbs new file mode 100644 index 00000000..29b6d9a4 --- /dev/null +++ b/sig/r3x/client/healthchecks_io/response.rbs @@ -0,0 +1,27 @@ +module R3x + module Client + class HealthchecksIO + class Response + def initialize: (untyped response) -> void + + def success?: () -> bool + + def status: () -> Integer + + def body: () -> String + + def headers: () -> untyped + + def body_limit: () -> Integer? + + def to_s: () -> String + + def inspect: () -> String + + private + + attr_reader response: untyped + end + end + end +end diff --git a/sig/r3x/client/http.rbs b/sig/r3x/client/http.rbs new file mode 100644 index 00000000..b619604f --- /dev/null +++ b/sig/r3x/client/http.rbs @@ -0,0 +1,33 @@ +module R3x + module Client + class Http + def self.with_persistence: (?verify_ssl: bool verify_ssl, ?timeout: Integer? timeout) { (R3x::Client::Http) -> untyped } -> untyped + + def self.httpx_options: (verify_ssl: bool verify_ssl, timeout: Integer? timeout) -> Hash[Symbol, untyped] + + def initialize: (?verify_ssl: bool verify_ssl, ?timeout: Integer? timeout) -> void + + def get: (String url, ?params: Hash[untyped, untyped] params, ?headers: Hash[untyped, untyped] headers) -> untyped + + def head: (String url, ?params: Hash[untyped, untyped] params, ?headers: Hash[untyped, untyped] headers) -> untyped + + def post: (String url, Hash[untyped, untyped] payload, ?headers: Hash[untyped, untyped] headers) -> untyped + + def download_file: (String url, ?headers: Hash[untyped, untyped] headers) -> R3x::Client::Http::DownloadedFile + + def upload_file: (String url, untyped file, ?file_field: String file_field, ?filename: String? filename, ?content_type: String? content_type, ?params: Hash[untyped, untyped] params, ?headers: Hash[untyped, untyped] headers) -> untyped + + private + + def filename_from_url: (String url, content_type: String? content_type) -> String + + def sniff_content_type: (untyped file_io) -> String? + + def default_filename: (untyped file_io) -> String? + + def rewind_file: (untyped file_io) -> nil + + def restore_file_position: (untyped file_io, ?Integer? position) -> nil + end + end +end diff --git a/sig/r3x/client/http/downloaded_file.rbs b/sig/r3x/client/http/downloaded_file.rbs new file mode 100644 index 00000000..ec20d036 --- /dev/null +++ b/sig/r3x/client/http/downloaded_file.rbs @@ -0,0 +1,16 @@ +module R3x + module Client + class Http + class DownloadedFile + attr_reader body: String + attr_reader content_type: String? + attr_reader filename: String + attr_reader url: String + + def initialize: (body: String body, content_type: String? content_type, filename: String filename, url: String url) -> void + + def to_io: () -> StringIO + end + end + end +end diff --git a/sig/r3x/client/llm.rbs b/sig/r3x/client/llm.rbs new file mode 100644 index 00000000..3c634c4e --- /dev/null +++ b/sig/r3x/client/llm.rbs @@ -0,0 +1,37 @@ +module R3x + module Client + class Llm + MAX_RETRIES: Integer + RETRY_INTERVAL: Float + RETRY_BACKOFF_FACTOR: Integer + DEFAULT_CHAT_OPTIONS: Hash[Symbol, untyped] + CHAT_OPTIONS_BY_PROVIDER: Hash[Symbol, Hash[Symbol, untyped]] + CHAT_OPTIONS_BY_PROVIDER_MUTEX: Mutex + + def self.chat_options_for: (Symbol provider) -> Hash[Symbol, untyped] + + def self.build_chat_options_for: (Symbol provider) -> Hash[Symbol, untyped] + + def initialize: ( + api_key: String api_key, + config_api_key_attr: String config_api_key_attr, + ?max_retries: Integer? max_retries, + ?retry_interval: Numeric? retry_interval, + ?retry_backoff_factor: Numeric? retry_backoff_factor + ) -> void + + def analyze_image: (String image_bytes, prompt: String prompt, model: String model, ?schema: untyped schema) -> untyped + + def message: (model: String model, prompt: String prompt, ?schema: untyped schema) -> untyped + + def classify: (text: String text, model: String model, categories: Hash[String, String] categories, ?include_reason: bool include_reason, ?allow_other: bool allow_other) -> untyped + + private + + attr_reader llm_context: untyped + attr_reader chat_options: Hash[Symbol, untyped] + + def ask_model: (model: String model, prompt: String prompt, schema: untyped schema, ?attachments: untyped attachments) -> untyped + end + end +end diff --git a/sig/r3x/client/llm/classifier.rbs b/sig/r3x/client/llm/classifier.rbs new file mode 100644 index 00000000..42b56575 --- /dev/null +++ b/sig/r3x/client/llm/classifier.rbs @@ -0,0 +1,21 @@ +module R3x + module Client + class Llm + class Classifier + def initialize: (untyped message_method) -> void + + def classify: (text: String text, model: String model, categories: Hash[String, String] categories, ?include_reason: bool include_reason, ?allow_other: bool allow_other) -> untyped + + private + + attr_reader message_method: untyped + + def build_categories: (Hash[String, String] user_categories, allow_other: bool allow_other) -> Hash[String, String] + + def build_schema: (Array[String] category_names, include_reason: bool include_reason) -> untyped + + def build_prompt: (String text, Hash[String, String] categories, include_reason: bool include_reason) -> String + end + end + end +end diff --git a/sig/r3x/client/llm/provider_configuration.rbs b/sig/r3x/client/llm/provider_configuration.rbs new file mode 100644 index 00000000..64a7fd81 --- /dev/null +++ b/sig/r3x/client/llm/provider_configuration.rbs @@ -0,0 +1,16 @@ +module R3x + module Client + class Llm + class ProviderConfiguration + API_KEY_SUFFIX: String + + attr_reader api_key: String + attr_reader config_api_key_attr: String + + def initialize: (api_key: String api_key, config_api_key_attr: String config_api_key_attr) -> void + + def self.resolve: (api_key_env: String api_key_env) -> ProviderConfiguration + end + end + end +end diff --git a/sig/r3x/client/llm/provider_registry.rbs b/sig/r3x/client/llm/provider_registry.rbs new file mode 100644 index 00000000..1dce3b2b --- /dev/null +++ b/sig/r3x/client/llm/provider_registry.rbs @@ -0,0 +1,13 @@ +module R3x + module Client + class Llm + module ProviderRegistry + def self.register!: () -> void + + def self.register_opencode_go!: () -> void + + def self.opencode_go_provider_class: () -> singleton(Class) + end + end + end +end diff --git a/sig/r3x/client/markdownify.rbs b/sig/r3x/client/markdownify.rbs new file mode 100644 index 00000000..14f5471c --- /dev/null +++ b/sig/r3x/client/markdownify.rbs @@ -0,0 +1,23 @@ +module R3x + module Client + class Markdownify + include R3x::Concerns::Logger + + DEFAULT_METHOD: String + + def initialize: (url: String url, ?method: String method, ?retain_images: bool retain_images) -> void + + def convert: () -> Hash[String, untyped] + + private + + attr_reader url: String + attr_reader method: String + attr_reader retain_images: bool + + def dry_run_result: () -> Hash[String, untyped] + + def connection: () -> untyped + end + end +end diff --git a/sig/r3x/client/miniflux.rbs b/sig/r3x/client/miniflux.rbs new file mode 100644 index 00000000..21eddb1d --- /dev/null +++ b/sig/r3x/client/miniflux.rbs @@ -0,0 +1,30 @@ +module R3x + module Client + class Miniflux + DEFAULT_URL_ENV: String + DEFAULT_API_KEY_ENV: String + + DEFAULT_STATUS: String + DEFAULT_LIMIT: Integer + DEFAULT_ORDER: String + DEFAULT_DIRECTION: String + + def initialize: (?url_env: String url_env, ?api_key_env: String api_key_env) -> void + + def entries: (?status: String status, ?limit: Integer limit, ?order: String order, ?direction: String direction, **untyped filters) -> Hash[untyped, untyped] + + def category_entries: (category_id: Integer | String category_id, ?status: String status, ?limit: Integer limit, ?order: String order, ?direction: String direction, **untyped filters) -> Hash[untyped, untyped] + + def mark_category_entries_as_read: (category_id: Integer | String category_id) -> bool + + private + attr_reader base_url: String + attr_reader api_key: String + + def get: (String path, **untyped params) -> Hash[untyped, untyped] + def put: (String path) -> true + def connection: () -> untyped + end + end +end + diff --git a/sig/r3x/client/ocr.rbs b/sig/r3x/client/ocr.rbs new file mode 100644 index 00000000..ac99ad68 --- /dev/null +++ b/sig/r3x/client/ocr.rbs @@ -0,0 +1,27 @@ +module R3x + module Client + class Ocr + ENDPOINT: String + BASE_URL: String + DEFAULT_API_KEY_ENV: String + + MIME_TYPES: Hash[String, String] + + def initialize: (?api_key_env: String api_key_env) -> void + + def parse: (untyped io_or_path, ?language: String? language, ?engine: Integer? engine, ?filetype: String? filetype, ?overlay: bool overlay) -> Ocr::Result + + private + + attr_reader api_key: String + + def connection: () -> untyped + + def detect_mime: (untyped io_or_path) -> String + + def build_params: (untyped io_or_path, String mime_type, language: String? language, engine: Integer? engine, overlay: bool overlay) -> Hash[Symbol, untyped] + + def binary_string?: (untyped value) -> bool + end + end +end diff --git a/sig/r3x/client/ocr/result.rbs b/sig/r3x/client/ocr/result.rbs new file mode 100644 index 00000000..5c9c30df --- /dev/null +++ b/sig/r3x/client/ocr/result.rbs @@ -0,0 +1,29 @@ +module R3x + module Client + class Ocr + class Result + include Enumerable[untyped] + + def initialize: (untyped body) -> void + + def text: () -> String + + def success?: () -> bool + + def partial?: () -> bool + + def exit_code: () -> Integer + + def processing_time_ms: () -> Integer? + + def each: () { (untyped) -> untyped } -> untyped + | () -> untyped + + private + + attr_reader body: untyped + attr_reader pages: Array[untyped] + end + end + end +end diff --git a/sig/r3x/client/prometheus.rbs b/sig/r3x/client/prometheus.rbs new file mode 100644 index 00000000..90275cec --- /dev/null +++ b/sig/r3x/client/prometheus.rbs @@ -0,0 +1,15 @@ +module R3x + module Client + class Prometheus + DEFAULT_URL_ENV: String + + def initialize: (?url_env: String url_env) -> void + + def query: (String promql) -> Prometheus::Result + + private + + attr_reader base_url: String + end + end +end diff --git a/sig/r3x/client/prometheus/result.rbs b/sig/r3x/client/prometheus/result.rbs new file mode 100644 index 00000000..9920562e --- /dev/null +++ b/sig/r3x/client/prometheus/result.rbs @@ -0,0 +1,20 @@ +module R3x + module Client + class Prometheus + class Result + include Enumerable[untyped] + + attr_reader result_type: String? + + def initialize: (untyped data) -> void + + def each: () { (untyped) -> untyped } -> untyped + | () -> untyped + + private + + attr_reader series: Array[untyped] + end + end + end +end diff --git a/sig/r3x/client/victoria_logs.rbs b/sig/r3x/client/victoria_logs.rbs new file mode 100644 index 00000000..cee07806 --- /dev/null +++ b/sig/r3x/client/victoria_logs.rbs @@ -0,0 +1,22 @@ +module R3x + module Client + class VictoriaLogs + DEFAULT_URL_ENV: String + DEFAULT_TIMEOUT: String + + def self.configured?: () -> bool + + def initialize: (?url_env: String url_env) -> void + def query: (query: String query, ?start_at: untyped start_at, ?end_at: untyped end_at, ?limit: Integer limit, ?timeout: String timeout) -> Array[Hash[String, untyped]] + + private + attr_reader client: untyped + attr_reader base_url: String + + def query_params: (query: String query, start_at: untyped start_at, end_at: untyped end_at, limit: Integer limit, timeout: String timeout) -> Hash[String, untyped] + def sort_query: (String query) -> String + def format_time: (untyped value) -> String? + def parse_json_lines: (untyped body) -> Array[Hash[String, untyped]] + end + end +end diff --git a/sig/r3x/client/word_press.rbs b/sig/r3x/client/word_press.rbs new file mode 100644 index 00000000..f151f89f --- /dev/null +++ b/sig/r3x/client/word_press.rbs @@ -0,0 +1,22 @@ +module R3x + module Client + class WordPress + API_PATH: String + + def initialize: (url: String url) -> void + + def posts: (?per_page: Integer per_page, **untyped params) -> untyped + + def post: (Integer | String id) -> untyped + + def pages: (?per_page: Integer per_page, **untyped params) -> untyped + + def page: (Integer | String id) -> untyped + + private + attr_reader base_url: String + + def get: (String path, **untyped params) -> untyped + end + end +end diff --git a/sig/r3x/external_stubs.rbs b/sig/r3x/external_stubs.rbs new file mode 100644 index 00000000..50a0bb7e --- /dev/null +++ b/sig/r3x/external_stubs.rbs @@ -0,0 +1,147 @@ +module HTTPX + def self.plugin: (*untyped plugins, **untyped keywords) -> untyped + def self.with: (?untyped options, **untyped keywords) -> untyped + def self.get: (String url, ?params: Hash[untyped, untyped] params, **untyped keywords) -> untyped + def self.post: (String url, ?json: untyped json, ?form: untyped form, ?body: untyped body, ?params: Hash[untyped, untyped] params, **untyped keywords) -> untyped + def self.head: (String url, **untyped keywords) -> untyped +end + + +module OpenSSL + module SSL + VERIFY_NONE: untyped + end +end + +module Rack + module Mime + MIME_TYPES: Hash[String, String] + end +end + +module RSS + module Parser + def self.parse: (String source, bool validate) -> untyped + end +end + +module R3x + module Concerns + module Logger + def logger: () -> untyped + end + end + + module Policy + def self.dry_run_for: (Symbol key, ?untyped dry_run) -> bool + end + + module Env + def self.fetch: (String name) -> String? + def self.fetch!: (String name) -> String + def self.fetch_boolean: (String name) -> bool? + def self.present?: (String name) -> bool + def self.secure_fetch: (String name, prefix: String | Regexp prefix) -> String + end + + module Client + module GoogleAuth + def self.require_gmail!: () -> void + def self.require_sheets!: () -> void + def self.from_env: (project: String project, scope: String scope) -> untyped + def self.resolve_scope: (String alias_name) -> String + end + end + + module GemLoader + def self.require: (String feature) -> bool + end +end + + +class Tempfile + def self.new: (Array[String?] basename) -> Tempfile + + def binmode: () -> Tempfile + + def write: (untyped content) -> Integer + + def rewind: () -> Integer + + def close: () -> nil + + def unlink: () -> nil +end + +module URI + def self.parse: (String uri) -> untyped +end + +module Marcel + module MimeType + def self.for: (untyped io) -> String? + end +end + +module MultiJSON + def self.parse: (String string, ?untyped options) -> untyped + def self.generate: (untyped object, ?untyped options) -> String +end + +module Google + module Apis + module GmailV1 + class Message + def initialize: (?raw: String raw) -> void + end + + class GmailService + def authorization=: (untyped authorization) -> untyped + end + end + + module SheetsV4 + class SheetsService + def authorization=: (untyped authorization) -> untyped + def get_spreadsheet_values: (String spreadsheet_id, String range) -> untyped + end + end + end +end + +module Mail + def self.new: () -> untyped + + class Part + def self.new: () { (Mail::Part) -> untyped } -> Mail::Part + def body: () -> untyped + def body=: (String? body) -> untyped + end +end + +module RubyLLM + module Provider + def self.providers: () -> Hash[Symbol, untyped] + + def self.register: (Symbol name, untyped provider) -> void + end + + module Providers + class OpenAI + end + end + + def self.context: () { (untyped config) -> untyped } -> untyped +end + +module SecureRandom + def self.uuid: () -> String +end + +module Base64 + def self.strict_encode64: (String data) -> String +end + +class String + def blank?: () -> bool +end diff --git a/sig/r3x/workflow/base.rbs b/sig/r3x/workflow/base.rbs new file mode 100644 index 00000000..009b40f5 --- /dev/null +++ b/sig/r3x/workflow/base.rbs @@ -0,0 +1,7 @@ +module R3x + module Workflow + class Base + def self.workflow_key: () -> String + end + end +end diff --git a/sig/r3x/workflow/context.rbs b/sig/r3x/workflow/context.rbs new file mode 100644 index 00000000..c683ca13 --- /dev/null +++ b/sig/r3x/workflow/context.rbs @@ -0,0 +1,16 @@ +module R3x + module Workflow + class Context + attr_reader trigger: untyped + attr_reader execution: R3x::Workflow::Execution + attr_reader workflow_class: singleton(R3x::Workflow::Base)? + attr_reader workflow_key: String + + def initialize: (trigger: untyped trigger, workflow_key: String workflow_key, ?workflow_class: singleton(R3x::Workflow::Base)? workflow_class) -> void + + def client: () -> singleton(R3x::Workflow::Context::Client) + + def durable_set: (?Symbol | String name, ?ttl: untyped ttl) -> R3x::Workflow::DurableSet + end + end +end diff --git a/sig/r3x/workflow/context/client.rbs b/sig/r3x/workflow/context/client.rbs new file mode 100644 index 00000000..47f64244 --- /dev/null +++ b/sig/r3x/workflow/context/client.rbs @@ -0,0 +1,67 @@ +module R3x + module Workflow + class Context + module Client + def http: (?verify_ssl: bool verify_ssl, ?timeout: Integer timeout) -> R3x::Client::Http + + def persistent_http: (?verify_ssl: bool verify_ssl, ?timeout: Integer timeout) { (R3x::Client::Http) -> untyped } -> untyped + + def prometheus: (?url_env: String url_env) -> R3x::Client::Prometheus + + def healthchecks_io: (String check_uuid, ?ping_endpoint: String? ping_endpoint, ?ping_endpoint_env: String ping_endpoint_env) -> R3x::Client::HealthchecksIO + + def apify: (?api_key_env: String api_key_env) -> R3x::Client::Apify + + def llm: (api_key_env: String api_key_env, ?max_retries: Integer? max_retries, ?retry_interval: Numeric? retry_interval, ?retry_backoff_factor: Numeric? retry_backoff_factor) -> R3x::Client::Llm + + def google_sheets: (spreadsheet_id: String spreadsheet_id, project: String project) -> R3x::Client::GoogleSheets + + def gmail: (project: String project) -> R3x::Client::Google::Gmail + + def ocr: (?api_key_env: String api_key_env) -> R3x::Client::Ocr + + def rss: (String url, ?timeout: Integer timeout) -> untyped + + def google_translate: (project: String project) -> R3x::Client::Google::Translate + + def miniflux: (?url_env: String url_env, ?api_key_env: String api_key_env) -> R3x::Client::Miniflux + + def wordpress: (url: String url) -> R3x::Client::WordPress + + def discord: (?webhook_url_env: String webhook_url_env) -> R3x::Client::Discord + + def markdownify: (url: String url, ?method: String method, ?retain_images: bool retain_images) -> String + + def self.http: (?verify_ssl: bool verify_ssl, ?timeout: Integer timeout) -> R3x::Client::Http + + def self.persistent_http: (?verify_ssl: bool verify_ssl, ?timeout: Integer timeout) { (R3x::Client::Http) -> untyped } -> untyped + + def self.prometheus: (?url_env: String url_env) -> R3x::Client::Prometheus + + def self.healthchecks_io: (String check_uuid, ?ping_endpoint: String? ping_endpoint, ?ping_endpoint_env: String ping_endpoint_env) -> R3x::Client::HealthchecksIO + + def self.apify: (?api_key_env: String api_key_env) -> R3x::Client::Apify + + def self.llm: (api_key_env: String api_key_env, ?max_retries: Integer? max_retries, ?retry_interval: Numeric? retry_interval, ?retry_backoff_factor: Numeric? retry_backoff_factor) -> R3x::Client::Llm + + def self.google_sheets: (spreadsheet_id: String spreadsheet_id, project: String project) -> R3x::Client::GoogleSheets + + def self.gmail: (project: String project) -> R3x::Client::Google::Gmail + + def self.ocr: (?api_key_env: String api_key_env) -> R3x::Client::Ocr + + def self.rss: (String url, ?timeout: Integer timeout) -> untyped + + def self.google_translate: (project: String project) -> R3x::Client::Google::Translate + + def self.miniflux: (?url_env: String url_env, ?api_key_env: String api_key_env) -> R3x::Client::Miniflux + + def self.wordpress: (url: String url) -> R3x::Client::WordPress + + def self.discord: (?webhook_url_env: String webhook_url_env) -> R3x::Client::Discord + + def self.markdownify: (url: String url, ?method: String method, ?retain_images: bool retain_images) -> String + end + end + end +end diff --git a/sig/r3x/workflow/durable_set.rbs b/sig/r3x/workflow/durable_set.rbs new file mode 100644 index 00000000..eb2e2e79 --- /dev/null +++ b/sig/r3x/workflow/durable_set.rbs @@ -0,0 +1,17 @@ +module R3x + module Workflow + class DurableSet + DEFAULT_TTL: untyped + + def initialize: (workflow_key: String workflow_key, ?name: Symbol | String name, ?ttl: untyped ttl) -> void + + def include?: (untyped member) -> bool + + def add: (untyped member, ?ttl: untyped ttl) -> untyped + + def add?: (untyped member, ?ttl: untyped ttl) -> untyped + + def delete: (untyped member) -> untyped + end + end +end diff --git a/sig/r3x/workflow/execution.rbs b/sig/r3x/workflow/execution.rbs new file mode 100644 index 00000000..3cca5e0f --- /dev/null +++ b/sig/r3x/workflow/execution.rbs @@ -0,0 +1,15 @@ +module R3x + module Workflow + class Execution + attr_reader workflow_key: String + attr_reader trigger_key: String? + attr_reader active_job_id: String? + + def initialize: (workflow_key: String, ?trigger_key: String?, ?active_job_id: String?) -> void + + def previous_run_at: () -> untyped + + def first_run?: () -> bool + end + end +end