Steep 2.0 cross-check triage (2026-05-03)
Triage of the results of adding Steep 2.0.0 as an independent bundle under tool/steep/ and running make steep-check to check lib/ against sig/. On the premise of keeping Rigor’s own make check clean, this separates from the external checker’s perspective which warnings are real mismatches Rigor should also detect, which are correctly not picked up by Rigor (= limits of Steep), and which are resolvable as false positives through more precise typing.
Summary
Section titled “Summary”- Input:
make steep-check - Counts: 17 files / 54 cases (
Ruby::MethodBodyTypeMismatch42,Ruby::MethodParameterMismatch9,RBS::DuplicatedMethodDefinition3) - Categories:
| Category | Count | Summary |
|---|---|---|
| A. Rigor should also detect (real sig drift) | 48 | Wrong return-type declarations, missing arguments, duplicated declarations. Resolvable with sig/ fixes. |
| B. Correctly not detected by Rigor (Steep-specific false positives) | 0 | None this round. All warnings are grounded in fact. |
| C. Resolvable as false positives through proper typing (will vanish with Rigor refinement) | 6 | Caused by failure to track Array() coercion and Data.define keyword synthesis. |
Below, the breakdown and recommended actions are summarised by category.
Category A — Rigor should also detect (real sig drift)
Section titled “Category A — Rigor should also detect (real sig drift)”Hand-written declarations on the sig/ side have failed to keep up with the implementation in lib/. The kind that should be detected if “strict on returns” from the Robustness Principle (docs/type-specification/robustness-principle.md) is straightforwardly applied.
A-1. Return type of predicate methods top / bot / dynamic (39 cases)
Section titled “A-1. Return type of predicate methods top / bot / dynamic (39 cases)”The predicate methods top / bot / dynamic exposed by each type carrier (Top, Bot, Dynamic, Constant, IntegerRange, Nominal, Singleton, Union, Difference, Refined, Intersection, Tuple, HashShape) return Trinary.yes/no/maybe in the implementation (lib/rigor/type/top.rb:26-36), but sig/rigor/type.rbs:11-13 declares:
def top: () -> Topdef bot: () -> Botdef dynamic: () -> DynamicThe meaning of the return type itself is being mistaken: the sig reads as “calling top returns a Top-type instance,” but in reality it is a predicate “is this type top?” returning Trinary.
- Should it be detected: Yes — a straightforward violation of strict on returns. Automatically detectable once Rigor is complete.
- Fix: align the sig side with
def top: () -> Trinaryetc. 13 files × 3 methods = 39 sites. - Scope of impact:
sig/rigor/type.rbsonly. No changes needed on thelib/side.
Note: the warnings for Refined#dynamic and Difference#dynamic show the body inference as (Type::Dynamic | Trinary), but this is the same cause (a side effect of Steep picking up the route where an inherited / delegated target wrongly returns Type::Dynamic) and disappears with the same fix.
A-2. Return value of IntegerRange#lower / upper (2 cases)
Section titled “A-2. Return value of IntegerRange#lower / upper (2 cases)”lib/rigor/type/integer_range.rb:67-71 represents NEG_INFINITY / POS_INFINITY with Symbol sentinels, so lower / upper can return Integer | Float | Symbol. Meanwhile sig/rigor/type.rbs:71-72 says () -> Numeric. Since Symbol is not a subtype of Numeric, this is inconsistent.
- Should it be detected: Yes — the return set is broader than declared, a clear strict-on-returns violation.
- Two possible fix paths:
- Sig to
() -> (Integer | Float | Symbol)(match the actual). - Replace impl with
Float::INFINITYsentinels and keep the sig’sNumeric.
- Sig to
- Which is preferable needs to be reconciled with ADR-3 (type representation), but as long as we adopt Symbol sentinels, updating the sig side is realistic for now.
A-3. Missing argument on record_declarations (1 case)
Section titled “A-3. Missing argument on record_declarations (1 case)”lib/rigor/inference/scope_indexer.rb:473 takes 4 arguments:
def record_declarations(node, qualified_prefix, identity_table, discovered)sig/rigor/inference.rbs:135 takes 3:
def self?.record_declarations: (untyped node, Array[String] qualified_prefix, Hash[untyped, Type::t] table) -> voidThe 4th argument discovered has dropped out of the sig — typical sig drift.
- Fix: add
Array[untyped] discovered(actual type to be confirmed) to the sig.
A-4. Duplicated declarations of RbsLoader#instance_definition / singleton_definition (3 cases)
Section titled “A-4. Duplicated declarations of RbsLoader#instance_definition / singleton_definition (3 cases)”In sig/rigor/environment.rbs:41,48 and same:43,49 the same-named methods are declared twice with conflicting return types untyped and untyped?:
def instance_definition: (String | Symbol class_name) -> untyped...def instance_definition: (String | Symbol class_name) -> untyped?The RBS spec does not allow duplication other than as overloads. Removing one of each line for instance_definition / singleton_definition and unifying on the untyped? side is reasonable (confirm whether the implementation can return nil).
- Should it be detected: Yes — an RBS-spec-level error; Rigor should error (or will error) the same way when reading through the RBS parser.
A-5. Required keywords on CLI#initialize (3 cases)
Section titled “A-5. Required keywords on CLI#initialize (3 cases)”def initialize(argv, out:, err:)def initialize: (?Array[String] argv, ?out: untyped, ?err: untyped) -> voidThe sig has argv, out:, err: all optional, but the impl has all required (out: has no default). A caller trusting the sig and calling CLI.new with no arguments fails with ArgumentError.
- Should it be detected: Yes — the contractual lenience is not respected by the impl.
- Resolution direction: following ADR-5 (Robustness Principle), loosen the impl side: align to
def initialize(argv = [], out: $stdout, err: $stderr). This also preserves the behaviour ofself.start.
Category B — Correctly not detected by Rigor (Steep-specific false positives)
Section titled “Category B — Correctly not detected by Rigor (Steep-specific false positives)”Zero hits this round. All warnings emitted by Steep 2.0 reflected some substantive mismatch. The reason no warnings hit the area where Rigor has intentionally decided “not to detect” under its own lenience policy is that the sig/ side has diverged considerably from the implementation through hand-writing, so basic contract violations were exposed before the lenience discussion was reached. After fixing the sigs and re-running, it is likely that cases classifiable under this category will appear (e.g. Steep refusing gradual acceptance via untyped).
Category C — Resolvable as false positives through proper typing
Section titled “Category C — Resolvable as false positives through proper typing”Warnings caused by Steep’s flow-sensitivity or its core-library modeling being coarse. False-positive-treated in the sense that they should disappear naturally once Rigor’s robustness principle and control-flow analysis (docs/type-specification/control-flow-analysis.md) are complete.
C-1. Array(union) coercion (1 case)
Section titled “C-1. Array(union) coercion (1 case)”lib/rigor/analysis/fact_store.rb:128:
def fact_targets(fact) Array(fact.target)endfact.target is Target | Array[Target]. Ruby’s Array() Kernel method has the convention “leave as-is if Array[T], wrap in [T] if T,” so the return value is naturally Array[Target]. Steep infers the return of Array() as [T | Array[T]] (a 1-element tuple), unable to step into specialization across union branches.
- Rigor perspective: this disappears by adding to the built-in catalog of
Kernel#Array(data/builtins/) the spec “when the argument is a union, normalise each member and unify.” - No immediate workaround needed — stays at warning under lenient settings as a false positive.
C-2. initialize overrides on Data.define-derived classes (5 cases)
Section titled “C-2. initialize overrides on Data.define-derived classes (5 cases)”Target / Fact in lib/rigor/analysis/fact_store.rb:26,32 put pre-processing on top of classes generated by Data.define(:kind, :name) etc. via def initialize(kind:, name:) .... Steep cannot fully analyze the keyword matching between Data.define’s auto-generated signatures and the hand-written overrides, raising MethodParameterMismatch in 5 places.
- Rigor perspective: it disappears by adding, in addition to specialised inference of
Data.define(*members)(docs/type-specification/structural-interfaces-and-object-shapes.md), a rule that prefers explicitly-writteninitializesignatures. This is a feature that fits directly into the v0.0.4 / v0.1.0 roadmap. - Workaround: writing
Target/Fact’sinitializeexplicitly in full on the sig side so it doesn’t contradictData’s auto-generated signature also silences Steep (the current sig side is already hand-written). What Steep can’t pick up is theData-derived composed signature; this is an area where Rigor can take the lead by having aData.define-specific recognizer.
Action proposal
Section titled “Action proposal”In priority order:
- Fix return types of predicate methods in
sig/rigor/type.rbs(A-1, 39 cases)- Close to mechanical replacement. Align to
def (top|bot|dynamic): () -> Trinary. - This alone removes 39 of 54 cases.
- Close to mechanical replacement. Align to
- Clean up duplicated declarations in
environment.rbs(A-4, 3 cases)- Delete the extra lines and unify on the
untyped?side.
- Delete the extra lines and unify on the
- Add the 4th argument to
scope_indexer’s sig (A-3, 1 case) - Fix
IntegerRange#lower/uppersig (A-2, 2 cases)- Short term: sig to
Integer | Float | Symbol. Long term: re-examine the sentinel representation in ADR-3.
- Short term: sig to
- Loosen
CLI#initialize(A-5, 3 cases)- Fix to
def initialize(argv = [], out: $stdout, err: $stderr).
- Fix to
- Leave Category C as lenient warnings for now. Re-run and confirm they vanish once Rigor’s
Kernel#Arraymodeling /Data.definerecognizer lands.
Reproduction
Section titled “Reproduction”# One-time (dependency resolution)nix develop --command make steep-install
# Runnix develop --command make steep-check
# Individual target (passing options)nix develop --command make steep ARGS="check --severity-level=error"The Steepfile is currently D::Ruby.lenient. This setting is for compatibility-check purposes — to comprehensively pick up warnings (= detect more broadly than strict). Whether to incorporate into the make verify chain in the future will be decided by looking at the remaining cases after resolving A-1 through A-5.
Pre-v0.1.1 release follow-up (2026-05-08)
Section titled “Pre-v0.1.1 release follow-up (2026-05-08)”A-1 through A-5 have all been landed in v0.1.x’s Track 4 (see docs/ROADMAP.md v0.1.1 Track 4 item 11 / 13 / 12). Re-run (make steep-check) results re-classified just before the v0.1.1 release:
- Input:
make steep-check(v0.1.1 release candidate branch) - Counts: 8 cases / 2 files (8 warnings only, 0 errors)
| Category | Count | Content |
|---|---|---|
| A. Real sig drift | 0 | A-1 through A-5 all resolved |
| B. Correctly not detected by Rigor | 8 | Data.define do ... end override block / Kernel#Array narrowing / def lambda default — due to limits of Steep’s Ruby idiom support |
| C. False positives (vanish with Rigor refinement) | 0 | Re-classified into Category B |
Error resolved in v0.1.1 (1 case)
Section titled “Error resolved in v0.1.1 (1 case)”sig/rigor.rbs:67RBS::UnknownTypeName: Rigor::Cache::Store—Rigor::Cache::Storeis still not properly sigged as of v0.1.1 (entered inUNSIGNED_NAMESPACES), so the reference was changed tountypedto bring it down. At the same timeattr_reader plugin_registry: untypedand?plugin_requirer: untypedwere added to theRunnerdeclaration (reflecting in the sig the Runner surface introduced in Track 2 slice 7).- Full sig of
Rigor::Cache::Storeis deferred as a v0.1.x maintenance task (to be written at the stage of removal fromUNSIGNED_NAMESPACES).
Classification of remaining 8 (warnings)
Section titled “Classification of remaining 8 (warnings)”All due to limits of Steep’s Ruby idiom recognition; no mismatches that need fixing on the Rigor codebase side:
| Count | Location | Kind | Nature |
|---|---|---|---|
| 5 | lib/rigor/analysis/fact_store.rb:26-32 | Ruby::MethodParameterMismatch | Target = Data.define(...) do def initialize(kind:, name:); ...; end; end — Steep tries to match the def initialize inside the override-block against the outer FactStore’s initialize declaration. A known Steep limitation that it cannot tie up with the Data subclass’s sig. Runtime correctly calls Data#initialize via super(...). |
| 1 | lib/rigor/analysis/fact_store.rb:128 | Ruby::MethodBodyTypeMismatch | Array(fact.target) with fact.target: Target | Array[Target] — Steep cannot narrow the Kernel#Array coercion to Array[Target] instead of [Target | Array[Target]]. Rewriting (fact.target.is_a?(Array) ? fact.target : [fact.target]) makes it disappear, but readability worsens, so deferred. |
| 2 | lib/rigor/plugin/loader.rb:41 | Ruby::MethodParameterMismatch | def self.load(configuration:, services:, requirer: ->(name) { require name }) — Steep mistakes the lambda default for the Kernel#require sig shape. Side effect of the Plugin namespace not having a sig (UNSIGNED_NAMESPACES). |
Conclusion
Section titled “Conclusion”make steep-check is stable at error 0 / warning 8. It is still not included in the make verify chain (intentional divergence at warning level is allowed). All warnings are expected to resolve once Plugin::* / Cache::Store sigs are properly developed.
© 2026 TypedDuck. Licensed under CC BY-SA 4.0.