rigortype 0.2.1 → 0.2.2

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 (105) 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 +557 -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 +532 -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.rb +25 -3
  89. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +124 -32
  90. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
  91. data/lib/rigor/inference/scope_indexer.rb +87 -89
  92. data/lib/rigor/plugin/isolation.rb +5 -5
  93. data/lib/rigor/plugin/loader.rb +4 -2
  94. data/lib/rigor/version.rb +1 -1
  95. data/skills/rigor-ask/SKILL.md +172 -0
  96. data/skills/rigor-doctor/SKILL.md +87 -0
  97. data/skills/rigor-editor-setup/SKILL.md +114 -0
  98. data/skills/rigor-mcp-setup/SKILL.md +117 -0
  99. data/skills/rigor-monkeypatch-resolve/SKILL.md +79 -0
  100. data/skills/rigor-next-steps/SKILL.md +113 -0
  101. data/skills/rigor-plugin-tune/SKILL.md +79 -0
  102. data/skills/rigor-protection-uplift/SKILL.md +133 -0
  103. data/skills/rigor-rbs-setup/SKILL.md +128 -0
  104. data/skills/rigor-upgrade/SKILL.md +79 -0
  105. metadata +90 -1
@@ -88,6 +88,7 @@ module Rigor
88
88
  to_h: :tuple_to_h,
89
89
  zip: :tuple_zip,
90
90
  :[] => :tuple_index,
91
+ slice: :tuple_index,
91
92
  fetch: :tuple_index,
92
93
  dig: :tuple_dig,
93
94
  values_at: :tuple_values_at,
@@ -842,7 +843,10 @@ module Rigor
842
843
 
843
844
  # `tuple.min` / `tuple.max` — fold when every element is
844
845
  # a `Constant` whose values share a Ruby-comparable
845
- # domain. Empty tuples fold to `Constant[nil]`.
846
+ # domain. Empty tuples fold to `Constant[nil]`. The 1-arg
847
+ # `min(n)` / `max(n)` form folds to a `Tuple` of the n
848
+ # edge-most values in Ruby's order (`min(n)` ascending,
849
+ # `max(n)` descending) — the n-arg sibling of `first(n)`.
846
850
  def tuple_min(tuple, _method_name, args)
847
851
  tuple_minmax(tuple, args, :min)
848
852
  end
@@ -852,7 +856,7 @@ module Rigor
852
856
  end
853
857
 
854
858
  def tuple_minmax(tuple, args, edge)
855
- return nil unless args.empty?
859
+ return tuple_minmax_n(tuple, args.first, edge) unless args.empty?
856
860
  return Type::Combinator.constant_of(nil) if tuple.elements.empty?
857
861
 
858
862
  values = constant_values(tuple.elements)
@@ -864,6 +868,25 @@ module Rigor
864
868
  nil
865
869
  end
866
870
 
871
+ # `tuple.min(n)` / `tuple.max(n)` — a `Tuple` of the n
872
+ # edge-most element values, delegating to Ruby's
873
+ # `Array#min` / `#max` for the ordering. Declines on a
874
+ # non-static / negative count or non-Constant elements.
875
+ # The result is bounded by the tuple's known arity, so no
876
+ # extra size cap is needed.
877
+ def tuple_minmax_n(tuple, arg, edge)
878
+ return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(Integer)
879
+ return nil if arg.value.negative?
880
+
881
+ values = constant_values(tuple.elements)
882
+ return nil if values.nil?
883
+
884
+ picked = values.public_send(edge, arg.value)
885
+ Type::Combinator.tuple_of(*picked.map { |v| Type::Combinator.constant_of(v) })
886
+ rescue StandardError
887
+ nil
888
+ end
889
+
867
890
  # `tuple.minmax` — the `[min, max]` pair as a 2-slot
868
891
  # `Tuple[Constant[min], Constant[max]]`, mirroring the
869
892
  # `Range#minmax` fold. Every element must be a `Constant`
@@ -1201,6 +1224,13 @@ module Rigor
1201
1224
  # indices still fall through because the same handler serves
1202
1225
  # `fetch`, while statically nil slices can be represented
1203
1226
  # precisely for `[]`.
1227
+ # `[]` and its exact alias `slice` share the index / Range /
1228
+ # start-length folding. `fetch` routes here too but stays
1229
+ # integer-index-only: the Range and start-length branches gate
1230
+ # on this selector set, which `fetch` is deliberately not in.
1231
+ SLICE_SELECTORS = Set[:[], :slice].freeze
1232
+ private_constant :SLICE_SELECTORS
1233
+
1204
1234
  def tuple_index(tuple, method_name, args)
1205
1235
  case args.size
1206
1236
  when 1 then tuple_single_index(tuple, method_name, args.first)
@@ -1211,17 +1241,18 @@ module Rigor
1211
1241
  def tuple_single_index(tuple, method_name, arg)
1212
1242
  return nil unless arg.is_a?(Type::Constant)
1213
1243
 
1214
- return tuple_range_slice(tuple, arg.value) if method_name == :[] && arg.value.is_a?(Range)
1215
- return nil unless arg.value.is_a?(Integer)
1244
+ value = arg.value
1245
+ return tuple_range_slice(tuple, value) if SLICE_SELECTORS.include?(method_name) && value.is_a?(Range)
1246
+ return nil unless value.is_a?(Integer)
1216
1247
 
1217
- idx = normalise_index(arg.value, tuple.elements.size)
1248
+ idx = normalise_index(value, tuple.elements.size)
1218
1249
  return nil unless idx
1219
1250
 
1220
1251
  tuple.elements[idx]
1221
1252
  end
1222
1253
 
1223
1254
  def tuple_start_length_slice(tuple, method_name, args)
1224
- return nil unless method_name == :[]
1255
+ return nil unless SLICE_SELECTORS.include?(method_name)
1225
1256
 
1226
1257
  start, length = args
1227
1258
  return nil unless start.is_a?(Type::Constant) && length.is_a?(Type::Constant)
@@ -118,10 +118,12 @@ module Rigor
118
118
  # table to suppress false positives for methods the
119
119
  # user has defined but no RBS sig describes. Merged
120
120
  # UNDER the cross-file pre-pass seed; details: merge_project_method_indexes.
121
- discovered_methods = deep_merge_class_methods(
122
- default_scope.discovered_methods, build_discovered_methods(root)
123
- )
124
- seeded_scope = seeded_scope.with_discovery(seeded_scope.discovery.with(discovered_methods: discovered_methods))
121
+ # One combined descent yields both the discovered-methods existence
122
+ # table and the instance def-node table — see
123
+ # {#build_methods_and_def_nodes}. `seed_discovered_methods` seeds the
124
+ # former onto the scope and returns the def-node table for
125
+ # `merge_project_method_indexes` below.
126
+ seeded_scope, file_def_nodes = seed_discovered_methods(seeded_scope, default_scope, root)
125
127
 
126
128
  # v0.0.2 #5 + ADR-24 slice 2 — record per-instance-method
127
129
  # def nodes, the class -> superclass map, and the
@@ -134,7 +136,7 @@ module Rigor
134
136
  # table. Seeded inside `merge_project_method_indexes` so the
135
137
  # per-file visibilities merge OVER the cross-file project seed
136
138
  # rather than overwriting it.
137
- seeded_scope = merge_project_method_indexes(seeded_scope, default_scope, root)
139
+ seeded_scope = merge_project_method_indexes(seeded_scope, default_scope, root, file_def_nodes)
138
140
 
139
141
  table = {}.compare_by_identity
140
142
  table.default = seeded_scope
@@ -160,6 +162,19 @@ module Rigor
160
162
  table
161
163
  end
162
164
 
165
+ # Runs the combined methods/def-nodes descent (one walk of the file),
166
+ # seeds the discovered-methods existence table onto `seeded_scope`
167
+ # (merged UNDER the cross-file pre-pass seed `default_scope` carries),
168
+ # and returns `[scope, file_def_nodes]` so the caller can thread the
169
+ # def-node table into {#merge_project_method_indexes} without walking
170
+ # the file a second time.
171
+ def seed_discovered_methods(seeded_scope, default_scope, root)
172
+ file_methods, file_def_nodes = build_methods_and_def_nodes(root)
173
+ discovered_methods = deep_merge_class_methods(default_scope.discovered_methods, file_methods)
174
+ scope = seeded_scope.with_discovery(seeded_scope.discovery.with(discovered_methods: discovered_methods))
175
+ [scope, file_def_nodes]
176
+ end
177
+
163
178
  # ADR-48 Struct slice 3 — installs the top-level fold-safe-local set
164
179
  # ({Inference::StructFoldSafety}). Struct member layouts of constant
165
180
  # receivers are resolved through the side-table the seeded scope carries.
@@ -179,9 +194,9 @@ module Rigor
179
194
  # `discovered_def_index_for_paths` seed carried on
180
195
  # `default_scope` — same-file declarations win per entry,
181
196
  # the cross-file seed supplies sibling-file ancestors.
182
- def merge_project_method_indexes(seeded_scope, default_scope, root)
197
+ def merge_project_method_indexes(seeded_scope, default_scope, root, file_def_nodes)
183
198
  def_nodes = default_scope.discovered_def_nodes.merge(
184
- build_discovered_def_nodes(root)
199
+ file_def_nodes
185
200
  ) { |_class, cross_file, per_file| cross_file.merge(per_file) }
186
201
  singleton_def_nodes = default_scope.discovered_singleton_def_nodes.merge(
187
202
  build_discovered_singleton_def_nodes(root)
@@ -1406,16 +1421,29 @@ module Rigor
1406
1421
  Type::Combinator.singleton_of(full)
1407
1422
  end
1408
1423
 
1409
- # Slice 7 phase 12 — in-source method discovery pre-pass.
1410
- # Walks every class/module body and records the methods
1411
- # introduced via `Prism::DefNode` (instance + singleton)
1412
- # and via recognised `define_method(:name) { ... }` calls.
1413
- # The returned table maps qualified class name to a
1414
- # `Hash[Symbol, :instance | :singleton]`.
1415
- def build_discovered_methods(root)
1416
- accumulator = {}
1417
- walk_methods(root, [], false, accumulator)
1418
- accumulator.transform_values(&:freeze).freeze
1424
+ # Slice 7 phase 12 — in-source method discovery pre-pass, fused with
1425
+ # the instance-method def-node pre-pass (v0.0.2 #5). One descent
1426
+ # produces BOTH tables the per-file `index` and the cross-file
1427
+ # pre-pass each need together:
1428
+ #
1429
+ # - `methods` : `{class_name => {method => :instance | :singleton}}`
1430
+ # for every `def` / `define_method(:name)` / `attr_*` / `alias` /
1431
+ # Data/Struct-member reader (the undefined-method existence table).
1432
+ # - `def_nodes` : `{class_name => {method => Prism::DefNode}}` for
1433
+ # every instance-side `def` (the inter-procedural return-inference
1434
+ # table; singleton defs and `define_method` are intentionally
1435
+ # skipped — `record_def_node` filters them).
1436
+ #
1437
+ # `walk_methods` and `walk_def_nodes` had byte-identical class /
1438
+ # module / singleton / meta-block descents (both stop at `DefNode`),
1439
+ # so a single combined walk records both accumulators at once instead
1440
+ # of traversing every file twice.
1441
+ def build_methods_and_def_nodes(root)
1442
+ methods = {}
1443
+ def_nodes = {}
1444
+ walk_methods_and_def_nodes(root, [], false, methods, def_nodes)
1445
+ apply_alias_def_nodes(root, def_nodes)
1446
+ [methods.transform_values(&:freeze).freeze, def_nodes.transform_values(&:freeze).freeze]
1419
1447
  end
1420
1448
 
1421
1449
  # Merges two `class_name => { method => kind }` tables, unioning
@@ -1431,7 +1459,14 @@ module Rigor
1431
1459
  end
1432
1460
 
1433
1461
  # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
1434
- def walk_methods(node, qualified_prefix, in_singleton_class, accumulator)
1462
+ # Combined `walk_methods` + `walk_def_nodes` descent. The two walks
1463
+ # had identical class / module / singleton-class / meta-block
1464
+ # traversals and both stopped at `DefNode`; the only divergences are
1465
+ # leaf actions (recorded into the right accumulator) and the original
1466
+ # `walk_methods` returning at `AliasMethodNode` (its symbol-only
1467
+ # children carry no def / class node, so not descending them is
1468
+ # byte-identical for `def_nodes` too). See {#build_methods_and_def_nodes}.
1469
+ def walk_methods_and_def_nodes(node, qualified_prefix, in_singleton_class, methods_acc, def_nodes_acc)
1435
1470
  return unless node.is_a?(Prism::Node)
1436
1471
 
1437
1472
  case node
@@ -1439,40 +1474,40 @@ module Rigor
1439
1474
  name = Source::ConstantPath.qualified_name(node.constant_path)
1440
1475
  if name
1441
1476
  child_prefix = qualified_prefix + [name]
1442
- record_meta_superclass_members(node, child_prefix, accumulator) if node.is_a?(Prism::ClassNode)
1443
- walk_methods(node.body, child_prefix, false, accumulator) if node.body
1477
+ record_meta_superclass_members(node, child_prefix, methods_acc) if node.is_a?(Prism::ClassNode)
1478
+ walk_methods_and_def_nodes(node.body, child_prefix, false, methods_acc, def_nodes_acc) if node.body
1444
1479
  return
1445
1480
  end
1446
1481
  when Prism::SingletonClassNode
1447
1482
  if node.body
1448
1483
  singleton_prefix = singleton_class_prefix(node, qualified_prefix)
1449
1484
  if singleton_prefix
1450
- walk_methods(node.body, singleton_prefix, true, accumulator)
1485
+ walk_methods_and_def_nodes(node.body, singleton_prefix, true, methods_acc, def_nodes_acc)
1451
1486
  return
1452
1487
  end
1453
1488
  end
1454
1489
  when Prism::ConstantWriteNode
1455
1490
  if meta_new_block_body(node)
1456
1491
  child_prefix = qualified_prefix + [node.name.to_s]
1457
- walk_methods(meta_new_block_body(node), child_prefix, false, accumulator)
1492
+ walk_methods_and_def_nodes(meta_new_block_body(node), child_prefix, false, methods_acc, def_nodes_acc)
1458
1493
  return
1459
1494
  end
1460
1495
  when Prism::DefNode
1461
- record_def_method(node, qualified_prefix, in_singleton_class, accumulator)
1496
+ record_def_method(node, qualified_prefix, in_singleton_class, methods_acc)
1497
+ record_def_node(node, qualified_prefix, in_singleton_class, def_nodes_acc)
1462
1498
  return
1463
1499
  when Prism::AliasMethodNode
1464
- record_alias_method(node, qualified_prefix, in_singleton_class, accumulator)
1500
+ record_alias_method(node, qualified_prefix, in_singleton_class, methods_acc)
1465
1501
  return
1466
1502
  when Prism::CallNode
1467
- record_define_method(node, qualified_prefix, in_singleton_class, accumulator) if node.name == :define_method
1503
+ record_define_method(node, qualified_prefix, in_singleton_class, methods_acc) if node.name == :define_method
1468
1504
  if ATTR_MACROS.include?(node.name)
1469
- record_attr_methods(node, qualified_prefix, in_singleton_class,
1470
- accumulator)
1505
+ record_attr_methods(node, qualified_prefix, in_singleton_class, methods_acc)
1471
1506
  end
1472
1507
  end
1473
1508
 
1474
1509
  node.compact_child_nodes.each do |child|
1475
- walk_methods(child, qualified_prefix, in_singleton_class, accumulator)
1510
+ walk_methods_and_def_nodes(child, qualified_prefix, in_singleton_class, methods_acc, def_nodes_acc)
1476
1511
  end
1477
1512
  end
1478
1513
 
@@ -1606,57 +1641,6 @@ module Rigor
1606
1641
  end
1607
1642
  end
1608
1643
 
1609
- # v0.0.2 #5 — instance-side def-node recording. Walks
1610
- # class bodies the same way as `build_discovered_methods`
1611
- # but records the actual `Prism::DefNode` for each
1612
- # **instance** method so `ExpressionTyper` can re-type
1613
- # the body at the call site for inter-procedural return
1614
- # inference. Singleton methods and `define_method` calls
1615
- # are intentionally skipped: the inference path needs a
1616
- # statically introspectable body, and singleton dispatch
1617
- # has its own complications (Class / Module ancestry)
1618
- # the first-iteration rule does not yet model.
1619
- def build_discovered_def_nodes(root)
1620
- accumulator = {}
1621
- walk_def_nodes(root, [], false, accumulator)
1622
- apply_alias_def_nodes(root, accumulator)
1623
- accumulator.transform_values(&:freeze).freeze
1624
- end
1625
-
1626
- def walk_def_nodes(node, qualified_prefix, in_singleton_class, accumulator)
1627
- return unless node.is_a?(Prism::Node)
1628
-
1629
- case node
1630
- when Prism::ClassNode, Prism::ModuleNode
1631
- name = Source::ConstantPath.qualified_name(node.constant_path)
1632
- if name
1633
- child_prefix = qualified_prefix + [name]
1634
- walk_def_nodes(node.body, child_prefix, false, accumulator) if node.body
1635
- return
1636
- end
1637
- when Prism::SingletonClassNode
1638
- if node.body
1639
- singleton_prefix = singleton_class_prefix(node, qualified_prefix)
1640
- if singleton_prefix
1641
- walk_def_nodes(node.body, singleton_prefix, true, accumulator)
1642
- return
1643
- end
1644
- end
1645
- when Prism::ConstantWriteNode
1646
- if meta_new_block_body(node)
1647
- child_prefix = qualified_prefix + [node.name.to_s]
1648
- walk_def_nodes(meta_new_block_body(node), child_prefix, false, accumulator)
1649
- return
1650
- end
1651
- when Prism::DefNode
1652
- record_def_node(node, qualified_prefix, in_singleton_class, accumulator)
1653
- return
1654
- end
1655
-
1656
- node.compact_child_nodes.each do |child|
1657
- walk_def_nodes(child, qualified_prefix, in_singleton_class, accumulator)
1658
- end
1659
- end
1660
1644
  # v0.0.3 A — sentinel key under which `record_def_node`
1661
1645
  # files DefNodes that live outside any class / module
1662
1646
  # body (top-level helpers, `def`s nested inside DSL
@@ -2385,7 +2369,13 @@ module Rigor
2385
2369
  # the override-visibility-reduced rule can read an ancestor's
2386
2370
  # visibility declared in a sibling file.
2387
2371
  def accumulate_project_index(acc, path, root)
2388
- merge_discovered_defs(acc[:def_nodes], acc[:def_sources], path, root)
2372
+ # One combined descent yields both the methods existence table and
2373
+ # the def-node table; the latter is also consumed by
2374
+ # `record_class_sources`, so a def-dense file is walked once here
2375
+ # instead of three times (methods + def-nodes ×2). See
2376
+ # {#build_methods_and_def_nodes}.
2377
+ file_methods, file_def_nodes = build_methods_and_def_nodes(root)
2378
+ merge_discovered_defs(acc[:def_nodes], acc[:def_sources], path, file_def_nodes)
2389
2379
  build_discovered_singleton_def_nodes(root).each do |class_name, methods|
2390
2380
  (acc[:singleton_def_nodes][class_name] ||= {}).merge!(methods)
2391
2381
  end
@@ -2395,20 +2385,28 @@ module Rigor
2395
2385
  includes.each do |class_name, mods|
2396
2386
  acc[:includes][class_name] = ((acc[:includes][class_name] || []) + mods).uniq
2397
2387
  end
2398
- record_class_sources(acc[:class_sources], path, root, superclasses, includes)
2399
- merge_class_keyed_index_tables(acc, root)
2388
+ record_class_sources(acc[:class_sources], path, root, superclasses, includes, file_def_nodes)
2389
+ merge_class_keyed_index_tables(acc, root, file_methods)
2390
+ merge_member_layout_tables(acc, root)
2391
+ end
2392
+
2393
+ # Folds one file's Data + Struct member-layout tables into the
2394
+ # cross-file accumulator (kept out of {#accumulate_project_index} to
2395
+ # hold its ABC budget).
2396
+ def merge_member_layout_tables(acc, root)
2400
2397
  acc[:data_member_layouts].merge!(build_data_member_layouts(root))
2401
2398
  acc[:struct_member_layouts].merge!(build_struct_member_layouts(root))
2402
2399
  end
2403
2400
 
2404
2401
  # Folds the per-class method-visibility and method-existence tables of
2405
2402
  # one file into the cross-file accumulator (kept out of
2406
- # {#accumulate_project_index} to hold its ABC budget).
2407
- def merge_class_keyed_index_tables(acc, root)
2403
+ # {#accumulate_project_index} to hold its ABC budget). `file_methods`
2404
+ # is the existence table from the combined methods/def-nodes descent.
2405
+ def merge_class_keyed_index_tables(acc, root, file_methods)
2408
2406
  build_discovered_method_visibilities(root).each do |class_name, table|
2409
2407
  (acc[:method_visibilities][class_name] ||= {}).merge!(table)
2410
2408
  end
2411
- build_discovered_methods(root).each do |class_name, table|
2409
+ file_methods.each do |class_name, table|
2412
2410
  (acc[:methods][class_name] ||= {}).merge!(table)
2413
2411
  end
2414
2412
  end
@@ -2423,13 +2421,13 @@ module Rigor
2423
2421
  # dependency recording (ADR-46). The class-declaration walk
2424
2422
  # (`collect_class_decls`) catches bodyless / def-less reopenings the
2425
2423
  # other three builders miss.
2426
- def record_class_sources(class_sources, path, root, superclasses, includes)
2424
+ def record_class_sources(class_sources, path, root, superclasses, includes, file_def_nodes)
2427
2425
  names = Set.new
2428
2426
  collect_class_decls(root, [], decls = {})
2429
2427
  names.merge(decls.keys)
2430
2428
  names.merge(superclasses.keys)
2431
2429
  names.merge(includes.keys)
2432
- names.merge(build_discovered_def_nodes(root).keys)
2430
+ names.merge(file_def_nodes.keys)
2433
2431
  names.each { |name| (class_sources[name] ||= Set.new) << path }
2434
2432
  end
2435
2433
 
@@ -2438,8 +2436,8 @@ module Rigor
2438
2436
  # seen `"path:line"` definition site in `def_sources` (ADR-17 —
2439
2437
  # the un-registered-project-patch signal `call.undefined-method`
2440
2438
  # and `rigor triage` key on).
2441
- def merge_discovered_defs(def_nodes, def_sources, path, root)
2442
- build_discovered_def_nodes(root).each do |class_name, methods|
2439
+ def merge_discovered_defs(def_nodes, def_sources, path, file_def_nodes)
2440
+ file_def_nodes.each do |class_name, methods|
2443
2441
  (def_nodes[class_name] ||= {}).merge!(methods)
2444
2442
  sources = (def_sources[class_name] ||= {})
2445
2443
  methods.each do |method_name, def_node|
@@ -12,13 +12,13 @@ module Rigor
12
12
  # `exe/rigor` launcher maps `.rigor.yml`'s `plugins_isolation:` onto it
13
13
  # before re-exec). Three backends behind one interface:
14
14
  #
15
- # - `none` (**default**) — load into the main space and call directly.
16
- # Lowest cost; no isolation. Fine for the common case because the
17
- # invoked library is trusted + pure.
15
+ # - `none` — load into the main space and call directly.
16
+ # Lowest cost; no isolation. Used as the fallback where fork is
17
+ # unavailable; fine because the invoked library is trusted + pure.
18
18
  # - `ruby_box` — call inside a {Box} (`Ruby::Box`, `RUBY_BOX=1`). Isolates
19
19
  # core-class monkey-patches + lets gem versions coexist, but a native
20
20
  # crash in the boxed work still takes the process down (in-process).
21
- # - `process` — call in a forked worker ({Process}); returns data over a
21
+ # - `process` (**default**) — call in a forked worker ({Process}); returns data over a
22
22
  # pipe. The strongest: a child crash (even `SIGSEGV`) is contained —
23
23
  # the parent survives and declines. Higher cost (fork + IPC).
24
24
  #
@@ -73,7 +73,7 @@ module Rigor
73
73
  end
74
74
 
75
75
  # `none` — load the trusted library into the main space and call it
76
- # directly. No isolation; lowest cost; the current default behaviour.
76
+ # directly. No isolation; lowest cost; the fork-unavailable fallback.
77
77
  module Direct
78
78
  module_function
79
79
 
@@ -183,8 +183,10 @@ module Rigor
183
183
  Plugin.registered_for(newly_registered.first)
184
184
  else
185
185
  raise LoadError.new(
186
- "plugin gem #{entry[:gem].inspect} registered multiple plugins " \
187
- "(#{newly_registered.sort.inspect}); disambiguate with an explicit `id:` field",
186
+ "plugin gem #{entry[:gem].inspect} bundles #{newly_registered.size} plugins " \
187
+ "(#{newly_registered.sort.inspect}) and cannot be activated as a single `plugins:` entry — " \
188
+ "it is a convenience meta-gem. List the individual plugin gems you want in `plugins:` " \
189
+ "(e.g. `rigor-#{newly_registered.min}`), or select one with an explicit `id:` field.",
188
190
  plugin_ref: entry[:gem]
189
191
  )
190
192
  end
data/lib/rigor/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rigor
4
- VERSION = "0.2.1"
4
+ VERSION = "0.2.2"
5
5
  end
@@ -0,0 +1,172 @@
1
+ ---
2
+ name: rigor-ask
3
+ description: |
4
+ Rigor is a niche, fast-moving Ruby type checker; its rules, flags, and type behaviour are version-specific, so what you "remember" about it is likely wrong or stale — do NOT answer from memory or guess. For ANY question about Rigor, use this skill and investigate procedurally: run `rigor docs` (handbook + manual, bundled OFFLINE and version-matched), `rigor explain` for a diagnostic id, and for the user's own code `rigor check` / `annotate` / `type-of`, then answer only from what you read. Covers: why a line is flagged or if it's a false positive; the type model (narrowing, refinements, `Dynamic`, RBS); config keys, flags, baselines; comparisons to Sorbet, Steep, mypy, PHPStan; whether it handles Rails, RSpec, or a gem; writing an RBS signature; "what is Rigor / why use it / is it right for us?". Trigger on any Rigor mention seeking understanding — even casual, comparative, or grumbling. Skip only when Rigor isn't mentioned, or it's purely "set it up / fix / reduce it for me" (→ rigor-next-steps).
5
+ license: MPL-2.0
6
+ metadata:
7
+ version: 0.2.0
8
+ homepage: https://github.com/rigortype/rigor
9
+ ---
10
+
11
+ # Ask Rigor anything
12
+
13
+ Someone has a question about Rigor. It might be about a diagnostic, the
14
+ type model, a flag, how Rigor stacks up against another type checker,
15
+ whether Rigor can handle their framework, how to type a method — or just
16
+ "what is this, and should I use it?" Whatever it is, **answer from the
17
+ source, not from memory.**
18
+
19
+ Two things make that easy, and you have both offline:
20
+
21
+ - **Rigor's own docs ship inside the gem.** `rigor docs` serves the full
22
+ handbook and manual, always matching the user's installed version, no
23
+ network. This is the authoritative copy: a rule's exact firing
24
+ condition, a flag's spelling, a config default — all drift release to
25
+ release, and `rigor docs` is the copy that shipped with *this* install,
26
+ so an answer drawn from it cannot disagree with the binary they run.
27
+ - **Rigor can read the user's actual code.** When the question is about
28
+ *their* program — "why is this flagged?", "what type does Rigor see
29
+ here?", "how well-typed is my project?" — `rigor check` / `annotate` /
30
+ `type-of` / `triage` / `coverage` answer from what Rigor inferred. A
31
+ concrete inferred type beats any abstract explanation.
32
+
33
+ This is the user's shortcut: they only ever need to remember two skills —
34
+ **`rigor-next-steps`** ("what should we do next?") and **`rigor-ask`**
35
+ ("answer this about Rigor"). They ask in plain language; *you* turn it
36
+ into the right lookup or analysis so they never have to remember the
37
+ command.
38
+
39
+ ## The toolbox
40
+
41
+ Everything here is read-only and needs no network.
42
+
43
+ ### Reading the docs
44
+
45
+ | Command | Use |
46
+ | --- | --- |
47
+ | `rigor docs` | The offline doc index (`llms.txt`) — the map. Start here when you don't know which page. |
48
+ | `rigor docs --list [manual\|handbook]` | List every bundled page with its path (optionally one category). |
49
+ | `rigor docs <name>` | Print a page. `<name>` is a category-qualified path (`handbook/03-narrowing`), a prefixed basename (`03-narrowing`), or a unique short name (`narrowing`). Pages that exist in **both** trees (e.g. `plugins`) must be qualified — `manual/07-plugins` vs `handbook/09-plugins`. |
50
+ | `rigor explain <rule>` | The catalogue entry for a diagnostic id (`rigor explain call.undefined-method`) — what it means, why it fires, how to address it. |
51
+
52
+ ### Grounding the answer in the user's code
53
+
54
+ | Command | Use |
55
+ | --- | --- |
56
+ | `rigor check <path>` | Run the analysis. Scope it to a file or directory for a quick answer — don't analyse the whole project just to settle one question. `--format json` exposes structured fields (`receiver_type`, `method_name`, `evidence_tier`, …). |
57
+ | `rigor annotate <file>` | Reprint the file with the inferred type of each line in the margin — *what Rigor actually sees*. |
58
+ | `rigor type-of <file>:<line>:<col>` | The inferred type at one position. |
59
+ | `rigor triage` | Cluster the project's diagnostics by rule / receiver / method — for "what's the shape of my errors?". |
60
+ | `rigor coverage [--protection]` | Type / type-protection coverage — for "how well-typed is this?" and "where are the holes?". |
61
+ | `rigor plugins` | Which plugins are installed and enabled *here* — the honest answer to "does Rigor support <gem/framework>?". |
62
+ | `rigor sig-gen <path>` | Generate RBS for code — for "how do I type this?". Offer it and show the result; this project prefers sig-gen over hand-written RBS. |
63
+
64
+ ## Where the answer lives
65
+
66
+ Classify the question, then go to the page(s) — and, for anything about
67
+ *their* code, the command(s) — that own it. When unsure where a page is,
68
+ `rigor docs` (the index) or `rigor docs --list handbook` routes you.
69
+
70
+ | The question is about… | Go to |
71
+ | --- | --- |
72
+ | **A specific diagnostic** — "why is this flagged?", "what does this error mean?", "is this a false positive?" | `rigor explain <rule>`, then `rigor docs diagnostics`. If it's *their* code, also `rigor annotate <file>` / `rigor type-of` to see the inferred types the rule fired on. |
73
+ | **The type model / a concept** — narrowing, refinements, tuple & hash shapes, `Dynamic`, RBS interop, lightweight HKT | The handbook: `rigor docs --list handbook`, then the chapter — `handbook/03-narrowing`, `04-tuples-and-shapes`, `07-rbs-and-extended`, `12-lightweight-hkt`, … |
74
+ | **Operating Rigor** — a config key, CLI flag, baseline, plugins, CI, caching | The manual: `rigor docs configuration`, `cli-reference`, `baseline`, `manual/07-plugins`, `ci`, `caching`, `troubleshooting`. |
75
+ | **How Rigor compares to another tool** — Sorbet, Steep, RBS, TypeScript, mypy, PHPStan, TypeProf, Go, Rust, Java/C# | The chapter/appendix written for exactly that: `handbook/10-sorbet`, `appendix-steep`, `appendix-typescript`, `appendix-mypy`, `appendix-phpstan`, `appendix-typeprof`, `appendix-rust`, `appendix-go`, `appendix-java-csharp` (`rigor docs --list handbook` shows them all). |
76
+ | **Whether Rigor can do X** — generics, Rails, RSpec, a specific gem, concurrency | The handbook for the language feature; for framework/gem support, **`rigor plugins`** (what's actually available in *this* install) plus the per-plugin page `rigor docs rigor-<gem>` (e.g. `rigor docs rigor-sidekiq`) and the catalogue `rigor docs --list manual`. |
77
+ | **Writing a type / RBS** — "how do I type this?", an annotation, a signature | Handbook `07-rbs-and-extended` + `11-sig-gen`; manual `rbs-extended-annotations`. Then offer `rigor sig-gen <path>` to generate it (preferred over hand-RBS) and show the output. |
78
+ | **What Rigor is / why use it / is it right for me** | Handbook `01-getting-started` for the pitch, `handbook/02-everyday-types` for a quick mental model of the type zoo. Ground "is it right for *my* project" in a scoped `rigor check` / `rigor coverage` so they see Rigor on their real code. |
79
+
80
+ ## Answer from the page, name the page
81
+
82
+ Quote or paraphrase the relevant passage and **say which page you drew
83
+ from** (e.g. "per `rigor docs handbook/03-narrowing` …"), so the user can
84
+ re-read it with the same command. Prefer the doc's own wording over a
85
+ remembered approximation. When you ran a command against their code, show
86
+ the relevant line of output — a concrete inferred type is more convincing
87
+ than prose, and it proves the answer rather than asserting it.
88
+
89
+ ## When the question is really "do X for me"
90
+
91
+ Some questions are a task in disguise: *"how do I get Rigor into CI?"*,
92
+ *"how do I shrink this baseline?"*, *"how do I set Rigor up here?"* The
93
+ useful reply is **short**: orient the user — what the thing is, the one
94
+ decision that actually matters, the rough shape of it — then **hand the
95
+ doing to the skill built for it.** Resist pasting the full procedure
96
+ inline (the entire CI workflow YAML, the whole baseline-reduction loop):
97
+ that skill owns the steps, keeps them correct, and updates as the tool
98
+ moves, so duplicating them here only bloats the answer and drifts out of
99
+ date. The line is *explaining* the thing (yours) versus *wiring it in*
100
+ (the setup skill's).
101
+
102
+ A good hand-off is two or three sentences of orientation plus the pointer:
103
+
104
+ - setup / "what next?" → **`rigor-next-steps`** (it probes the project and routes)
105
+ - CI → **`rigor-ci-setup`** · editor → **`rigor-editor-setup`** · MCP agent → **`rigor-mcp-setup`**
106
+ - baseline reduction → **`rigor-baseline-reduce`** · coverage holes → **`rigor-protection-uplift`**
107
+ - a missing gem/DSL → **`rigor-plugin-author`** · monkey-patch clusters → **`rigor-monkeypatch-resolve`**
108
+
109
+ When in doubt, give less and point — it respects the user's "two skills
110
+ to remember" promise and keeps each answer to the part only `rigor-ask`
111
+ can give.
112
+
113
+ ## If the docs don't cover it
114
+
115
+ The bundled set is the **drive-Rigor** corpus (manual + handbook). The
116
+ normative **type specification**, the internal spec, and the ADRs are
117
+ contributor-facing and stay web-only — they are *not* in `rigor docs`; if
118
+ a question genuinely needs them, say so and point at
119
+ <https://rigor.typedduck.fail/llms.txt> rather than guessing.
120
+
121
+ But before you defer, remember Rigor installs from RubyGems **with its
122
+ full source** — the per-plugin pages under the gem's `docs/manual/plugins/`,
123
+ the analyzer and plugin code under `lib/`. For a detail no doc page spells
124
+ out (a plugin's exact rule, a default baked into the code), reading the
125
+ bundled file directly is a perfectly good way to ground the answer, and
126
+ beats a guess. The rule that never bends: **never invent a flag, rule id,
127
+ config key, behaviour, or command output** — read the page, read the
128
+ source, or run the command, and quote only output you actually saw. A
129
+ confident wrong answer about a type checker is worse than "let me check."
130
+
131
+ ## Examples
132
+
133
+ **A diagnostic on their code** — *"Why is Rigor flagging `s.lenght`?"*
134
+
135
+ ```sh
136
+ rigor explain call.undefined-method # what the rule means and why it fires
137
+ rigor annotate demo.rb # the inferred type of `s` on that line
138
+ ```
139
+
140
+ Answer from both: Rigor inferred a concrete `String` receiver for `s`,
141
+ and `String` has no `lenght` (a typo for `length`) — grounded in what
142
+ `annotate` showed, not in a guess.
143
+
144
+ **A comparison** — *"How is Rigor different from Sorbet?"*
145
+
146
+ ```sh
147
+ rigor docs handbook/10-sorbet # the chapter written for this
148
+ ```
149
+
150
+ Answer from the chapter's framing (RBS-superset, gradual `Dynamic`,
151
+ inference-first) rather than a remembered summary, and name it so they
152
+ can read on.
153
+
154
+ **A capability** — *"Does Rigor understand our Sidekiq workers?"*
155
+
156
+ ```sh
157
+ rigor plugins # is rigor-sidekiq enabled in THIS project?
158
+ rigor docs rigor-sidekiq # the per-plugin page: what it teaches Rigor
159
+ ```
160
+
161
+ Answer from what's actually installed, plus — if useful — a scoped
162
+ `rigor check app/workers` so they see Rigor on their real workers.
163
+
164
+ **Authoring** — *"How do I type this method?"*
165
+
166
+ ```sh
167
+ rigor sig-gen path/to/file.rb # generate the RBS, show it
168
+ rigor docs handbook/07-rbs-and-extended
169
+ ```
170
+
171
+ Generate it, show the signature, and explain it from the handbook —
172
+ preferring sig-gen's output over hand-written RBS.