rigortype 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -14
  3. data/docs/handbook/01-getting-started.md +311 -0
  4. data/docs/handbook/02-everyday-types.md +337 -0
  5. data/docs/handbook/03-narrowing.md +359 -0
  6. data/docs/handbook/04-tuples-and-shapes.md +321 -0
  7. data/docs/handbook/05-methods-and-blocks.md +339 -0
  8. data/docs/handbook/06-classes.md +305 -0
  9. data/docs/handbook/07-rbs-and-extended.md +427 -0
  10. data/docs/handbook/08-understanding-errors.md +373 -0
  11. data/docs/handbook/09-plugins.md +241 -0
  12. data/docs/handbook/10-sorbet.md +347 -0
  13. data/docs/handbook/11-sig-gen.md +312 -0
  14. data/docs/handbook/12-lightweight-hkt.md +333 -0
  15. data/docs/handbook/README.md +275 -0
  16. data/docs/handbook/appendix-elixir.md +370 -0
  17. data/docs/handbook/appendix-go.md +399 -0
  18. data/docs/handbook/appendix-java-csharp.md +470 -0
  19. data/docs/handbook/appendix-liskov.md +580 -0
  20. data/docs/handbook/appendix-mypy.md +370 -0
  21. data/docs/handbook/appendix-phpstan.md +338 -0
  22. data/docs/handbook/appendix-protocols-and-structural-typing.md +292 -0
  23. data/docs/handbook/appendix-rust.md +446 -0
  24. data/docs/handbook/appendix-steep.md +336 -0
  25. data/docs/handbook/appendix-type-theory.md +1662 -0
  26. data/docs/handbook/appendix-typeprof.md +416 -0
  27. data/docs/handbook/appendix-typescript.md +332 -0
  28. data/docs/install.md +189 -0
  29. data/docs/llms.txt +72 -0
  30. data/docs/manual/01-installation.md +342 -0
  31. data/docs/manual/02-cli-reference.md +569 -0
  32. data/docs/manual/03-configuration.md +152 -0
  33. data/docs/manual/04-diagnostics.md +206 -0
  34. data/docs/manual/05-inspecting-types.md +109 -0
  35. data/docs/manual/06-baseline.md +104 -0
  36. data/docs/manual/07-plugins.md +92 -0
  37. data/docs/manual/08-skills.md +143 -0
  38. data/docs/manual/09-editor-integration.md +245 -0
  39. data/docs/manual/10-mcp-server.md +539 -0
  40. data/docs/manual/11-ci.md +274 -0
  41. data/docs/manual/12-caching.md +116 -0
  42. data/docs/manual/13-troubleshooting.md +120 -0
  43. data/docs/manual/14-rails-quickstart.md +332 -0
  44. data/docs/manual/15-type-protection-coverage.md +204 -0
  45. data/docs/manual/16-rbs-extended-annotations.md +190 -0
  46. data/docs/manual/17-driving-improvement.md +160 -0
  47. data/docs/manual/README.md +87 -0
  48. data/docs/manual/ci-templates/README.md +58 -0
  49. data/docs/manual/plugins/README.md +86 -0
  50. data/docs/manual/plugins/rigor-actioncable.md +78 -0
  51. data/docs/manual/plugins/rigor-actionmailer.md +74 -0
  52. data/docs/manual/plugins/rigor-actionpack.md +80 -0
  53. data/docs/manual/plugins/rigor-activejob.md +58 -0
  54. data/docs/manual/plugins/rigor-activerecord.md +102 -0
  55. data/docs/manual/plugins/rigor-activestorage.md +74 -0
  56. data/docs/manual/plugins/rigor-activesupport-core-ext.md +86 -0
  57. data/docs/manual/plugins/rigor-devise.md +70 -0
  58. data/docs/manual/plugins/rigor-dry-schema.md +56 -0
  59. data/docs/manual/plugins/rigor-dry-struct.md +60 -0
  60. data/docs/manual/plugins/rigor-dry-types.md +59 -0
  61. data/docs/manual/plugins/rigor-dry-validation.md +62 -0
  62. data/docs/manual/plugins/rigor-factorybot.md +76 -0
  63. data/docs/manual/plugins/rigor-graphql.md +89 -0
  64. data/docs/manual/plugins/rigor-hanami.md +83 -0
  65. data/docs/manual/plugins/rigor-mangrove.md +73 -0
  66. data/docs/manual/plugins/rigor-minitest.md +86 -0
  67. data/docs/manual/plugins/rigor-pundit.md +72 -0
  68. data/docs/manual/plugins/rigor-rails-i18n.md +92 -0
  69. data/docs/manual/plugins/rigor-rails-routes.md +94 -0
  70. data/docs/manual/plugins/rigor-rails.md +44 -0
  71. data/docs/manual/plugins/rigor-rbs-inline.md +83 -0
  72. data/docs/manual/plugins/rigor-rspec-rails.md +72 -0
  73. data/docs/manual/plugins/rigor-rspec.md +86 -0
  74. data/docs/manual/plugins/rigor-shoulda-matchers.md +78 -0
  75. data/docs/manual/plugins/rigor-sidekiq.md +78 -0
  76. data/docs/manual/plugins/rigor-sinatra.md +61 -0
  77. data/docs/manual/plugins/rigor-sorbet.md +63 -0
  78. data/docs/manual/plugins/rigor-statesman.md +75 -0
  79. data/docs/manual/plugins/rigor-typescript-utility-types.md +71 -0
  80. data/exe/rigor +1 -1
  81. data/lib/rigor/analysis/incremental_session.rb +4 -2
  82. data/lib/rigor/analysis/run_stats.rb +13 -1
  83. data/lib/rigor/analysis/runner.rb +54 -12
  84. data/lib/rigor/cli/check_command.rb +1 -1
  85. data/lib/rigor/cli/docs_command.rb +248 -0
  86. data/lib/rigor/cli/skill_command.rb +103 -41
  87. data/lib/rigor/cli/skill_describe.rb +346 -0
  88. data/lib/rigor/cli/triage_command.rb +8 -2
  89. data/lib/rigor/cli/triage_renderer.rb +4 -0
  90. data/lib/rigor/cli.rb +25 -3
  91. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +124 -32
  92. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
  93. data/lib/rigor/inference/scope_indexer.rb +87 -89
  94. data/lib/rigor/plugin/isolation.rb +5 -5
  95. data/lib/rigor/plugin/loader.rb +4 -2
  96. data/lib/rigor/triage/catalogue.rb +16 -1
  97. data/lib/rigor/triage.rb +30 -7
  98. data/lib/rigor/version.rb +1 -1
  99. data/skills/rigor-ask/SKILL.md +172 -0
  100. data/skills/rigor-doctor/SKILL.md +87 -0
  101. data/skills/rigor-editor-setup/SKILL.md +114 -0
  102. data/skills/rigor-mcp-setup/SKILL.md +117 -0
  103. data/skills/rigor-monkeypatch-resolve/SKILL.md +79 -0
  104. data/skills/rigor-next-steps/SKILL.md +113 -0
  105. data/skills/rigor-plugin-tune/SKILL.md +79 -0
  106. data/skills/rigor-protection-uplift/SKILL.md +133 -0
  107. data/skills/rigor-rbs-setup/SKILL.md +128 -0
  108. data/skills/rigor-upgrade/SKILL.md +79 -0
  109. metadata +90 -1
@@ -0,0 +1,61 @@
1
+ # rigor-sinatra
2
+
3
+ Recognises Sinatra's class-level route DSL (`get` / `post` / `put`
4
+ / `delete` / `head` / `options` / `patch` / `link` / `unlink`) on
5
+ `Sinatra::Base` subclasses and narrows the route block's `self` so
6
+ its bare helpers — `params`, `redirect`, `halt`, `session`,
7
+ `headers`, `content_type`, `erb`, `status`, `body`, … — resolve
8
+ through `Sinatra::Base`'s RBS instead of going unresolved.
9
+
10
+ It ships bundled in `rigortype`. Activate it under `plugins:`:
11
+
12
+ ```yaml
13
+ plugins:
14
+ - rigor-sinatra
15
+ ```
16
+
17
+ ## What it does
18
+
19
+ ```ruby
20
+ class MyApp < Sinatra::Base
21
+ get "/users/:id" do
22
+ halt 404 unless params["id"]
23
+ redirect "/users/#{params['id']}/profile"
24
+ end
25
+ end
26
+ ```
27
+
28
+ Inside the route block, `self` is narrowed to `MyApp` (which
29
+ inherits from `Sinatra::Base`), so `params` / `redirect` / `halt`
30
+ resolve through the normal RBS chain. Without the plugin the block
31
+ body is typed as `Singleton[MyApp]` and that per-block resolution is
32
+ lost.
33
+
34
+ Sinatra's own RBS needs to be available for the helpers to resolve
35
+ — through Rigor's Bundler-awareness, a vendored sig, or (in the
36
+ demo) a local stub.
37
+
38
+ ## No diagnostics, no config
39
+
40
+ The plugin only recognises the route shape and narrows `self`; it
41
+ emits no diagnostics and has no config keys (the verb match table
42
+ is fixed in the manifest).
43
+
44
+ ## Limitations
45
+
46
+ - **No routing diagnostics** — path-pattern uniqueness, conflict
47
+ detection, and named-route reverse lookup are out of scope.
48
+ - **`helpers do … end`** blocks (injecting instance methods) are
49
+ not handled (that's Tier B / C work, not this Tier A shape).
50
+ - **`configure` / `set` settings DSL** is not handled.
51
+ - **Classic-style top-level routes** — a bare `get '/x' do … end`
52
+ with no enclosing `class < Sinatra::Base` — are deferred; the
53
+ recognition needs the receiver's class visible at the call site.
54
+
55
+ ## Plugin internals
56
+
57
+ The declarative `BlockAsMethod` manifest and the macro-substrate
58
+ `self`-narrowing it rides on are documented in the
59
+ [plugin's README](../../../plugins/rigor-sinatra/README.md). To
60
+ write a plugin, see [`examples/`](../../../examples/README.md) and
61
+ the [`rigor-plugin-author`](../08-skills.md) skill.
@@ -0,0 +1,63 @@
1
+ # rigor-sorbet
2
+
3
+ Lets Rigor read an existing [Sorbet](https://sorbet.org/) codebase
4
+ as a type source: inline `sig { ... }` blocks, RBI files, and the
5
+ `T.let` / `T.cast` / `T.must` / `T.unsafe` / `T.bind` /
6
+ `T.assert_type!` / `T.absurd` assertion forms are translated into
7
+ Rigor's own carriers, so you can run `rigor check` alongside
8
+ `srb tc` without rewriting anything in RBS. It reads source only —
9
+ it does not load `sorbet-runtime`.
10
+
11
+ It ships bundled in `rigortype`. Activate it under `plugins:`:
12
+
13
+ ```yaml
14
+ plugins:
15
+ - rigor-sorbet
16
+ ```
17
+
18
+ > **Full guide.** This page is the operational quick reference.
19
+ > The complete walkthrough — the Sorbet→Rigor type-vocabulary
20
+ > table, every assertion form, RBI / Tapioca-DSL handling, sigil
21
+ > semantics, `T.absurd` exhaustiveness, and the migration
22
+ > patterns — is
23
+ > [handbook chapter 10 — Coexisting with Sorbet](../../handbook/10-sorbet.md).
24
+
25
+ ## Configuration
26
+
27
+ ```yaml
28
+ plugins:
29
+ - gem: rigor-sorbet
30
+ config:
31
+ enforce_sigil: true # default; honour `# typed:` sigils
32
+ rbi_paths: ["sorbet/rbi"] # default; set [] to disable RBI loading
33
+ ```
34
+
35
+ - **`enforce_sigil`** (default `true`) — mirror Sorbet's own
36
+ contract: only record sigs from files at `# typed: true` or
37
+ stricter. Set `false` to record sigs from every parseable file
38
+ regardless of sigil. The inline assertion recognisers (`T.let`,
39
+ `T.cast`, …) always fire, since the user wrote them deliberately.
40
+ - **`rbi_paths`** (default `["sorbet/rbi"]`) — directories of
41
+ `.rbi` files to load (the standard Tapioca subdirectories
42
+ `gems/` / `annotations/` / `dsl/` / `shims/` participate by
43
+ recursion). Set `[]` to opt out, or add a vendored tree.
44
+
45
+ ## Scope and limits
46
+
47
+ The plugin is **input-side only**: it translates Sorbet's syntax
48
+ into Rigor's type model. It does **not** run Sorbet's checker,
49
+ ship `sorbet-runtime`, or enforce Sorbet's runtime guarantees.
50
+ When an RBS sig and a Sorbet sig disagree, RBS wins (the Sorbet
51
+ sig may refine but not contradict it). Forms outside the
52
+ translation table (`T.proc`, `T.self_type`, `T::Struct` /
53
+ `T::Enum` subclasses, …) degrade to `Dynamic[top]`. Chapter 10
54
+ documents the full vocabulary and these edges.
55
+
56
+ ## Plugin internals
57
+
58
+ The slice-by-slice implementation (sig parsing, assertion
59
+ lifting, the RBI tree walker, mixin-chain resolution, the
60
+ dispatcher tier ordering), the source layout, and the demo are in
61
+ the [plugin's README](../../../plugins/rigor-sorbet/README.md).
62
+ The design rationale is
63
+ [ADR-11](../../adr/11-sorbet-input-adapter.md).
@@ -0,0 +1,75 @@
1
+ # rigor-statesman
2
+
3
+ Validates `transition_to(:state)` calls against the states declared
4
+ in a `state_machine do … end` block, within the same file. A
5
+ transition to an undeclared state is flagged (with a did-you-mean
6
+ suggestion). It reads source only — no Statesman runtime
7
+ dependency. (The DSL method names are configurable, so it also fits
8
+ AASM-shaped state machines.)
9
+
10
+ It ships bundled in `rigortype`. Activate it under `plugins:`:
11
+
12
+ ```yaml
13
+ plugins:
14
+ - rigor-statesman
15
+ ```
16
+
17
+ ## What it checks
18
+
19
+ ```ruby
20
+ class Order
21
+ state_machine do
22
+ state :draft, initial: true
23
+ state :submitted
24
+ state :approved
25
+ end
26
+ end
27
+
28
+ order.transition_to(:submitted) # info: known state
29
+ order.transition_to(:approval) # error: unknown state :approval (did you mean :approved?)
30
+ order.transition_to(:purgatory) # error: unknown state :purgatory
31
+ ```
32
+
33
+ | Rule | Severity | Fires when |
34
+ | --- | --- | --- |
35
+ | `plugin.statesman.known-state` | info | `transition_to(:sym)` where `:sym` is declared in an in-file `state_machine` block |
36
+ | `plugin.statesman.unknown-state` | error | `transition_to(:sym)` where `:sym` is not declared (with a `Base.suggest` did-you-mean) |
37
+
38
+ Multiple `state_machine` blocks in one file union their states.
39
+ Non-literal-symbol arguments and files with no state machine are
40
+ silently passed through.
41
+
42
+ ## Configuration
43
+
44
+ ```yaml
45
+ plugins:
46
+ - gem: rigor-statesman
47
+ config:
48
+ dsl_method: "state_machine" # default; the block-opening method
49
+ state_method: "state" # default; the state-declaring method
50
+ transition_method: "transition_to" # default; the call to validate
51
+ ```
52
+
53
+ Renaming these adapts the plugin to a different state-machine DSL
54
+ (e.g. AASM's `aasm do … state … end`).
55
+
56
+ ## Limitations
57
+
58
+ - **File-scoped.** States declared in `models/order.rb` aren't
59
+ visible from another file — each file validates independently.
60
+ - **Literal symbols only.** A variable or method-call argument is
61
+ not checked.
62
+ - **Receiver-agnostic.** The check fires on any receiver as long as
63
+ *some* state machine in the file declares the symbol; it does not
64
+ tie a `transition_to` to a specific machine.
65
+
66
+ ## Plugin internals
67
+
68
+ `rigor-statesman` is the reference example for the two-pass
69
+ (collect-then-validate) pattern: a `node_file_context` pass
70
+ collects the declared states once, and a `node_rule` validates each
71
+ `transition_to` over the engine-owned walk. The layout, demo, and
72
+ contract surfaces are in the
73
+ [plugin's README](../../../plugins/rigor-statesman/README.md). To
74
+ write a plugin, see [`examples/`](../../../examples/README.md) and
75
+ the [`rigor-plugin-author`](../08-skills.md) skill.
@@ -0,0 +1,71 @@
1
+ # rigor-typescript-utility-types
2
+
3
+ Maps the TypeScript-canonical utility-type spellings onto Rigor's own
4
+ shape-projection type functions, so a codebase migrating from
5
+ TypeScript / Sorbet RBI can write the familiar names inside
6
+ `RBS::Extended` annotations. It registers five
7
+ [ADR-13](../../adr/13-typenode-resolver-plugin.md) `TypeNodeResolver`s
8
+ as a pure translation layer; the shape *semantics* live in core, where
9
+ they have one spec-owned definition shared by every consumer.
10
+
11
+ It ships bundled in `rigortype`. Activate it under `plugins:`:
12
+
13
+ ```yaml
14
+ plugins:
15
+ - rigor-typescript-utility-types
16
+ ```
17
+
18
+ > **Full guide.** The shape projections themselves — what each does,
19
+ > the lossy-projection rule, and the TypeScript→Rigor vocabulary table
20
+ > — are covered in the handbook:
21
+ > [chapter 4 — Tuples and hash shapes](../../handbook/04-tuples-and-shapes.md)
22
+ > § "Deriving new shapes" and the
23
+ > [TypeScript appendix](../../handbook/appendix-typescript.md). This page
24
+ > is the operational quick reference.
25
+
26
+ ## What it resolves
27
+
28
+ | TypeScript spelling | Rigor core projection |
29
+ | --- | --- |
30
+ | `Pick<T, K>` | `pick_of[T, K]` |
31
+ | `Omit<T, K>` | `omit_of[T, K]` |
32
+ | `Partial<T>` | `partial_of[T]` |
33
+ | `Required<T>` | `required_of[T]` |
34
+ | `Readonly<T>` | `readonly_of[T]` |
35
+
36
+ ```ruby
37
+ class Address
38
+ # @rbs!
39
+ # %a{rigor:v1:return: Pick[Address::Shape, :name | :email]}
40
+ def public_fields; end
41
+ end
42
+ ```
43
+
44
+ Once the plugin is active, the `Pick[…]` head resolves through the full
45
+ resolution pass (built-ins → plugin chain → RBS Nominal fallback), so
46
+ the sub-arguments may use any of Rigor's existing type vocabulary.
47
+
48
+ ## No configuration
49
+
50
+ The plugin has no configuration knobs — the resolver chain is
51
+ registered at class load.
52
+
53
+ ## Limitations
54
+
55
+ - **Unmapped TS names degrade to `Nominal`.** `Parameters<F>`,
56
+ `ReturnType<F>`, `InstanceType<C>`, `Awaited<P>`, the string-casing
57
+ utilities (`Uppercase`/`Lowercase`/…), `ThisParameterType`, and
58
+ `NoInfer` are not mapped — they have no Rigor analogue yet (or need a
59
+ core operator that hasn't landed) and resolve as `Nominal[Name, […]]`.
60
+ - **Lossy on non-shape carriers.** A projection applied to a bare
61
+ `Nominal[Hash, [K, V]]` (rather than a `HashShape` / `Tuple`) returns
62
+ the input unchanged and records a `dynamic.shape.lossy-projection`
63
+ `:info` diagnostic so you can audit the call site.
64
+
65
+ ## Plugin internals
66
+
67
+ The five resolver classes, the recursive resolution mechanism, and the
68
+ ADR-13 `TypeNodeResolver` contract are in the
69
+ [plugin's README](../../../plugins/rigor-typescript-utility-types/README.md).
70
+ To write a plugin, see [`examples/`](../../../examples/README.md) and the
71
+ [`rigor-plugin-author`](../08-skills.md) skill.
data/exe/rigor CHANGED
@@ -8,7 +8,7 @@
8
8
  # alias) we re-exec the same command with the flag set before anything
9
9
  # else loads. The `RUBY_BOX` guard prevents an infinite re-exec, and the
10
10
  # inherited environment (RUBYOPT / BUNDLE_*) preserves the Bundler
11
- # context across the exec. The `none` (default) and `process` strategies
11
+ # context across the exec. The `none` and `process` (default) strategies
12
12
  # need no flag, so this is a no-op for them — behaviour is unchanged
13
13
  # unless a project opts into `ruby_box`.
14
14
  rigor_isolation = ENV["RIGOR_PLUGIN_ISOLATION"].to_s
@@ -182,8 +182,10 @@ module Rigor
182
182
  @symbol_fingerprints = payload.symbol_fingerprints || {}
183
183
  # ADR-46 slice 3 — restore negative edges if present (absent in
184
184
  # pre-slice-3 snapshots → empty, which only loses the appeared-symbol
185
- # re-check refinement; the fingerprint still drops the snapshot on a
186
- # file add/remove, so it is never unsound).
185
+ # re-check refinement; such a snapshot is never loaded by a slice-3+
186
+ # engine because the engine version + schema is part of the snapshot
187
+ # fingerprint, and `--verify-incremental` backstops any residual
188
+ # under-capture, so it is never unsound).
187
189
  @missing = payload.missing || {}
188
190
  @class_decls = payload.class_decls || {}
189
191
  @symbol_dependents = Incremental.invert_symbols(@symbol_sources)
@@ -144,7 +144,19 @@ module Rigor
144
144
  out.puts("#{prefix} Ruby source files: #{@target_files}")
145
145
  out.puts("#{prefix}Type universe (symbol discovery; not analyzed for diagnostics)")
146
146
  out.puts("#{prefix} RBS classes available: #{@rbs_classes_total}")
147
- if @rbs_attribution_available
147
+ if @rbs_classes_total.zero?
148
+ # A normal run always loads the bundled core+stdlib RBS (~1300+
149
+ # classes), so zero means the environment failed to build (most
150
+ # often a duplicate declaration in `signature_paths:`) and fell
151
+ # back to empty — type coverage is then near-useless but the run
152
+ # still "succeeds". Surface it loudly so a broken setup is not
153
+ # read as a clean analysis (the 20260620 field trial: redmine
154
+ # would otherwise wire a 0-coverage check into CI).
155
+ out.puts("#{prefix} WARNING: the RBS environment is empty — it failed to build or loaded no")
156
+ out.puts("#{prefix} signatures, so type coverage is severely limited (most diagnostics")
157
+ out.puts("#{prefix} and coverage cannot fire). Usually a duplicate declaration in")
158
+ out.puts("#{prefix} `signature_paths:` — fix it and re-run; the rigor-doctor skill helps.")
159
+ elsif @rbs_attribution_available
148
160
  out.puts("#{prefix} project sig/: #{@rbs_classes_project_sig}")
149
161
  out.puts("#{prefix} bundled (core+stdlib+gems): #{@rbs_classes_bundled}")
150
162
  elsif @rbs_classes_total.positive?
@@ -483,19 +483,48 @@ module Rigor
483
483
  end
484
484
  private :run_project_pre_passes, :adopt_prebuilt_project_scan, :apply_pre_passes_result
485
485
 
486
+ # Ruby versions probed (ascending) to discover the lowest one this
487
+ # Prism build accepts for `version:`. Prism exposes no version
488
+ # list, so the floor is found empirically — only when a
489
+ # misconfigured `target_ruby` is rejected — so the diagnostic can
490
+ # name it instead of leaving the user to guess (the dogfood field
491
+ # trial, 20260620-skill-driven-onboarding-dogfood.md, burned cycles
492
+ # on exactly this).
493
+ PRISM_VERSION_LADDER = %w[
494
+ 3.0.0 3.1.0 3.2.0 3.3.0 3.4.0 3.5.0 4.0.0 4.1.0 4.2.0
495
+ ].freeze
496
+
497
+ # @return [String, nil] the lowest `target_ruby` this Prism build
498
+ # accepts, or nil if none of the ladder parses. Memoised per
499
+ # process (the value is constant for a given Prism).
500
+ def self.prism_supported_floor
501
+ @prism_supported_floor ||= PRISM_VERSION_LADDER.find do |candidate|
502
+ Prism.parse("nil", version: candidate)
503
+ true
504
+ rescue ArgumentError
505
+ false
506
+ end
507
+ end
508
+
486
509
  # `target_ruby` flows through to Prism's `version:` option.
487
- # Prism enforces the supported range and raises
488
- # `ArgumentError` for versions it does not recognise. Run a
489
- # one-time smoke parse here so a misconfigured target_ruby
490
- # surfaces as a single project-level diagnostic instead of
491
- # crashing the whole run on the first file.
510
+ # Prism enforces the supported range and raises `ArgumentError`
511
+ # for versions it does not recognise. Run a one-time smoke parse
512
+ # here so a misconfigured target_ruby surfaces as a single
513
+ # project-level diagnostic instead of crashing the whole run on
514
+ # the first file and name the supported floor + where to read
515
+ # the right value, so the fix is obvious without a guess-and-retry
516
+ # loop.
492
517
  def validate_target_ruby
493
518
  Prism.parse("nil", version: @configuration.target_ruby)
494
519
  nil
495
520
  rescue ArgumentError => e
521
+ floor = self.class.prism_supported_floor
496
522
  Diagnostic.new(
497
523
  path: ".rigor.yml", line: 1, column: 1,
498
- message: "target_ruby #{@configuration.target_ruby.inspect} is not accepted by Prism: #{e.message}",
524
+ message: "target_ruby #{@configuration.target_ruby.inspect} is not supported by this Rigor build " \
525
+ "(Prism accepts #{floor} and newer). Set target_ruby to your project's Ruby version " \
526
+ "(>= #{floor}) — read it from Gemfile.lock's `RUBY VERSION` or .ruby-version. " \
527
+ "(Prism: #{e.message})",
499
528
  severity: :error,
500
529
  rule: "configuration-error",
501
530
  source_family: :builtin
@@ -734,7 +763,7 @@ module Rigor
734
763
  # cleanly with no output, which silently masked typos.
735
764
  def expand_paths(paths)
736
765
  files = []
737
- errors = []
766
+ bad = []
738
767
  Array(paths).each do |path|
739
768
  if File.directory?(path)
740
769
  files.concat(reject_excluded(Dir.glob(File.join(path, RUBY_GLOB))))
@@ -745,12 +774,25 @@ module Rigor
745
774
  elsif accept_as_ruby_file?(path)
746
775
  files << path
747
776
  elsif File.exist?(path)
748
- errors << path_error(path, "not a Ruby file (expected `.rb` or a directory)")
777
+ bad << [path, "not a Ruby file (expected `.rb` or a directory)"]
749
778
  else
750
- errors << path_error(path, "no such file or directory")
779
+ bad << [path, "no such file or directory"]
751
780
  end
752
781
  end
753
- { files: files, errors: errors }
782
+ { files: files, errors: path_expansion_errors(bad, any_files: files.any?) }
783
+ end
784
+
785
+ # A bad path *among valid ones* is a warn-and-skip — the run still
786
+ # does useful work, so `rigor check app lib` with no `lib/` (the
787
+ # 20260620 field trial's strap case) analyses `app` instead of
788
+ # aborting on exit 1. A bad path that leaves NOTHING to analyse
789
+ # stays an error so a lone typo (`rigor check typo.rb`) is not
790
+ # silently masked (the regression the path-error check was added
791
+ # to catch in the first place).
792
+ def path_expansion_errors(bad, any_files:)
793
+ severity = any_files ? :warning : :error
794
+ suffix = any_files ? " (skipped)" : ""
795
+ bad.map { |path, message| path_error(path, "#{message}#{suffix}", severity: severity) }
754
796
  end
755
797
 
756
798
  def accept_as_ruby_file?(path)
@@ -778,13 +820,13 @@ module Rigor
778
820
  @configuration.exclude_patterns.any? { |pattern| File.fnmatch?(pattern, path) }
779
821
  end
780
822
 
781
- def path_error(path, message)
823
+ def path_error(path, message, severity: :error)
782
824
  Diagnostic.new(
783
825
  path: path,
784
826
  line: 1,
785
827
  column: 1,
786
828
  message: message,
787
- severity: :error
829
+ severity: severity
788
830
  )
789
831
  end
790
832
 
@@ -799,7 +799,7 @@ module Rigor
799
799
  end
800
800
 
801
801
  def ci_detected_hint(platform)
802
- tail = "see `rigor skill print rigor-ci-setup`"
802
+ tail = "see `rigor skill rigor-ci-setup`"
803
803
  if platform.native_artifact?
804
804
  "rigor: #{platform.name} detected — for the inline report run " \
805
805
  "`rigor check --format #{platform.format}` and publish it as the platform's report artifact (#{tail})."