rigortype 0.2.9 → 0.3.0

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 (184) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/docs/handbook/03-narrowing.md +2 -1
  4. data/docs/handbook/04-tuples-and-shapes.md +7 -3
  5. data/docs/handbook/06-classes.md +18 -9
  6. data/docs/handbook/08-understanding-errors.md +1 -0
  7. data/docs/handbook/11-sig-gen.md +11 -1
  8. data/docs/handbook/appendix-mypy.md +4 -3
  9. data/docs/install.md +14 -6
  10. data/docs/manual/01-installation.md +98 -4
  11. data/docs/manual/02-cli-reference.md +49 -12
  12. data/docs/manual/03-configuration.md +34 -2
  13. data/docs/manual/04-diagnostics.md +23 -0
  14. data/docs/manual/07-plugins.md +4 -4
  15. data/docs/manual/09-editor-integration.md +4 -4
  16. data/docs/manual/11-ci.md +51 -0
  17. data/docs/manual/12-caching.md +65 -0
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
  19. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
  20. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
  21. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
  22. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  23. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
  24. data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
  25. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  26. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
  27. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
  28. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
  29. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +93 -0
  30. data/lib/rigor/analysis/check_rules.rb +557 -95
  31. data/lib/rigor/analysis/dependency_recorder.rb +93 -9
  32. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  33. data/lib/rigor/analysis/incremental_session.rb +456 -51
  34. data/lib/rigor/analysis/path_expansion.rb +42 -0
  35. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
  36. data/lib/rigor/analysis/rule_catalog.rb +226 -1
  37. data/lib/rigor/analysis/run_cache_key.rb +68 -0
  38. data/lib/rigor/analysis/run_cache_probe.rb +72 -0
  39. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
  40. data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
  41. data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
  42. data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
  43. data/lib/rigor/analysis/runner.rb +343 -68
  44. data/lib/rigor/analysis/severity_stamp.rb +43 -0
  45. data/lib/rigor/analysis/worker_session.rb +30 -2
  46. data/lib/rigor/bleeding_edge.rb +37 -8
  47. data/lib/rigor/builtins/regex_refinement.rb +36 -1
  48. data/lib/rigor/cache/descriptor.rb +88 -28
  49. data/lib/rigor/cache/file_digest.rb +158 -0
  50. data/lib/rigor/cache/incremental_snapshot.rb +41 -5
  51. data/lib/rigor/cache/rbs_descriptor.rb +56 -10
  52. data/lib/rigor/cache/store.rb +33 -3
  53. data/lib/rigor/ci_detector.rb +90 -0
  54. data/lib/rigor/cli/annotate_command.rb +15 -9
  55. data/lib/rigor/cli/baseline_command.rb +4 -1
  56. data/lib/rigor/cli/check_command.rb +212 -27
  57. data/lib/rigor/cli/coverage_command.rb +6 -1
  58. data/lib/rigor/cli/docs_command.rb +0 -30
  59. data/lib/rigor/cli/doctor_command.rb +163 -23
  60. data/lib/rigor/cli/lsp_command.rb +5 -0
  61. data/lib/rigor/cli/mcp_command.rb +5 -0
  62. data/lib/rigor/cli/plugins_command.rb +36 -6
  63. data/lib/rigor/cli/plugins_renderer.rb +44 -12
  64. data/lib/rigor/cli/probe_environment.rb +85 -0
  65. data/lib/rigor/cli/sig_gen_command.rb +36 -9
  66. data/lib/rigor/cli/skill_command.rb +1 -32
  67. data/lib/rigor/cli/trace_command.rb +5 -6
  68. data/lib/rigor/cli/type_of_command.rb +9 -8
  69. data/lib/rigor/cli/type_scan_command.rb +8 -8
  70. data/lib/rigor/cli.rb +15 -4
  71. data/lib/rigor/config_audit.rb +48 -2
  72. data/lib/rigor/configuration/severity_profile.rb +31 -0
  73. data/lib/rigor/configuration.rb +156 -8
  74. data/lib/rigor/environment/default_libraries.rb +35 -0
  75. data/lib/rigor/environment/rbs_loader.rb +164 -5
  76. data/lib/rigor/environment.rb +19 -27
  77. data/lib/rigor/inference/budget_trace.rb +77 -2
  78. data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
  79. data/lib/rigor/inference/def_handle.rb +23 -0
  80. data/lib/rigor/inference/def_node_resolver.rb +90 -0
  81. data/lib/rigor/inference/def_return_typer.rb +2 -1
  82. data/lib/rigor/inference/expression_typer.rb +361 -113
  83. data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
  84. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
  85. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
  86. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
  87. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +11 -0
  88. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
  89. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
  90. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
  91. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
  92. data/lib/rigor/inference/method_dispatcher.rb +57 -1
  93. data/lib/rigor/inference/mutation_widening.rb +42 -8
  94. data/lib/rigor/inference/narrowing.rb +127 -17
  95. data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
  96. data/lib/rigor/inference/project_patched_scanner.rb +2 -1
  97. data/lib/rigor/inference/rbs_type_translator.rb +8 -1
  98. data/lib/rigor/inference/scope_indexer.rb +451 -36
  99. data/lib/rigor/inference/statement_evaluator.rb +64 -15
  100. data/lib/rigor/inference/struct_fold_safety.rb +48 -11
  101. data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
  102. data/lib/rigor/inference/void_origin.rb +25 -0
  103. data/lib/rigor/inference/void_tail_summary.rb +220 -0
  104. data/lib/rigor/language_server/completion_provider.rb +2 -1
  105. data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
  106. data/lib/rigor/language_server/folding_range_provider.rb +2 -1
  107. data/lib/rigor/language_server/selection_range_provider.rb +2 -1
  108. data/lib/rigor/language_server/signature_help_provider.rb +2 -1
  109. data/lib/rigor/plugin/base.rb +9 -22
  110. data/lib/rigor/plugin/inflector.rb +12 -3
  111. data/lib/rigor/plugin/io_boundary.rb +5 -1
  112. data/lib/rigor/plugin/isolation.rb +81 -11
  113. data/lib/rigor/plugin/load_error.rb +10 -1
  114. data/lib/rigor/plugin/loader.rb +96 -14
  115. data/lib/rigor/plugin/node_rule_walk.rb +5 -3
  116. data/lib/rigor/plugin/registry.rb +32 -23
  117. data/lib/rigor/plugin.rb +26 -0
  118. data/lib/rigor/protection/mutator.rb +3 -2
  119. data/lib/rigor/reflection.rb +64 -0
  120. data/lib/rigor/runtime/jit.rb +128 -0
  121. data/lib/rigor/scope/discovery_index.rb +12 -2
  122. data/lib/rigor/scope.rb +122 -28
  123. data/lib/rigor/sig_gen/classification.rb +5 -1
  124. data/lib/rigor/sig_gen/generator.rb +38 -6
  125. data/lib/rigor/sig_gen/observation_collector.rb +4 -3
  126. data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
  127. data/lib/rigor/sig_gen/renderer.rb +10 -0
  128. data/lib/rigor/sig_gen/write_result.rb +9 -4
  129. data/lib/rigor/sig_gen/writer.rb +24 -2
  130. data/lib/rigor/source/node_children.rb +116 -0
  131. data/lib/rigor/source/node_locator.rb +3 -1
  132. data/lib/rigor/source/node_walker.rb +4 -2
  133. data/lib/rigor/source.rb +1 -0
  134. data/lib/rigor/type/difference.rb +28 -24
  135. data/lib/rigor/type/hash_shape.rb +34 -10
  136. data/lib/rigor/version.rb +1 -1
  137. data/lib/rigortype.rb +24 -0
  138. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
  139. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
  140. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
  141. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
  142. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
  143. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
  144. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
  145. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
  146. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
  147. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
  148. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
  149. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
  150. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
  151. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
  152. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
  153. data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
  154. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +3 -1
  155. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
  156. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +7 -5
  157. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
  158. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
  159. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
  160. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
  161. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
  162. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
  163. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
  164. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
  165. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
  166. data/sig/prism_node_children.rbs +9 -0
  167. data/sig/rigor/inference.rbs +7 -0
  168. data/sig/rigor/plugin/base.rbs +2 -3
  169. data/sig/rigor/reflection.rbs +1 -0
  170. data/sig/rigor/scope.rbs +12 -1
  171. data/skills/rigor-ci-setup/SKILL.md +10 -0
  172. data/skills/rigor-editor-setup/SKILL.md +11 -0
  173. data/skills/rigor-next-steps/SKILL.md +23 -2
  174. data/skills/rigor-plugin-author/SKILL.md +3 -3
  175. data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
  176. data/skills/rigor-plugin-review/SKILL.md +2 -2
  177. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
  178. data/skills/rigor-project-init/SKILL.md +29 -3
  179. data/skills/rigor-project-init/references/01-detect.md +1 -1
  180. data/skills/rigor-project-init/references/02-configure.md +39 -5
  181. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
  182. data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
  183. metadata +42 -24
  184. data/lib/rigor/cli/ci_detector.rb +0 -89
@@ -116,6 +116,18 @@ module Rigor
116
116
  errors.map { |error| parse_error_diagnostic(path, error) }
117
117
  end
118
118
 
119
+ # ADR-88 WD2 — the per-run catalog is an ADR-60 record-and-validate producer. Before this it was rebuilt
120
+ # unconditionally on every `--incremental` recheck (parse + walk every `.rb` / `.rbi` sig tree); as a
121
+ # producer it is cached to disk keyed on the plugin config, with `watch:` covering the scanned trees so an
122
+ # added / removed / edited sig file recomputes it. The producer VALUE (a Marshal-clean bundle of the
123
+ # catalog + the sigil map + the parse-error tuples) is what ADR-88 WD1 digests into the incremental
124
+ # fact-surface fingerprint, so a sig edit OUTSIDE `signature_paths:` invalidates the snapshot for free.
125
+ # The bundle is returned (not written to ivars) because a cache HIT does not run the block — the sigil map
126
+ # and parse errors would otherwise be lost on the warm path.
127
+ producer :catalog, watch: -> { catalog_watch_globs } do |_params|
128
+ build_catalog_bundle
129
+ end
130
+
119
131
  # ADR-52 slice 4 — per-call return-type path via the method-name-gated `dynamic_return` DSL. The
120
132
  # recognised name set is only known at run time (the catalog's `def` names come from the lazy
121
133
  # catalog build), so it is declared as a callable: the engine `instance_exec`s it once per run on
@@ -389,35 +401,69 @@ module Rigor
389
401
  end
390
402
  end
391
403
 
404
+ # ADR-88 WD2 — resolve the catalog from the `:catalog` producer (disk-cached) and unpack its Marshal-clean
405
+ # bundle into the ivars the return-type / sigil-gate / parse-error paths read. `producer_value` memoises
406
+ # per instance and rescues a producer failure to nil, hence the empty-bundle fallback.
392
407
  def ensure_catalog
393
408
  return @catalog if @catalog_built
394
409
 
410
+ bundle = producer_value(:catalog) || EMPTY_CATALOG_BUNDLE
411
+ @catalog = bundle[:catalog]
412
+ @sigil_by_path = bundle[:sigil_by_path]
413
+ @parse_errors_by_path = bundle[:parse_errors_by_path]
414
+ @catalog_built = true
415
+ @catalog
416
+ end
417
+
418
+ # ADR-88 WD2 — the `watch:` coverage for the `:catalog` producer: the `.rb` sig trees + the `.rbi` trees.
419
+ # An added / removed file under either invalidates the cached bundle even though the block globbed the
420
+ # tree itself. Evaluated at `cache_for` time (after `#init`), so `@configured_paths` / `@rbi_paths` are set.
421
+ def catalog_watch_globs
422
+ [[@configured_paths, "**/*.rb"], [@rbi_paths, "**/*.rbi"]]
423
+ end
424
+
425
+ # ADR-88 WD2 — build the Marshal-clean catalog bundle the `:catalog` producer caches. Harvests the `.rb`
426
+ # sig trees then the `.rbi` trees (last-wins ordering preserved from the ivar-mutating original) and
427
+ # returns `{ catalog:, sigil_by_path:, parse_errors_by_path: }`.
428
+ def build_catalog_bundle
395
429
  catalog = Catalog.new
430
+ sigil_by_path = {}
431
+ parse_errors_by_path = {}
396
432
  # Project source — `.rb` only.
397
- @configured_paths.each { |root| harvest_path(root, catalog, extensions: %w[.rb]) }
433
+ @configured_paths.each { |root| harvest_path(root, catalog, sigil_by_path, parse_errors_by_path, %w[.rb]) }
398
434
  # Sorbet RBI tree — `.rbi` only. Slice 4 of ADR-11.
399
- @rbi_paths.each { |root| harvest_path(root, catalog, extensions: %w[.rbi]) }
435
+ @rbi_paths.each { |root| harvest_path(root, catalog, sigil_by_path, parse_errors_by_path, %w[.rbi]) }
400
436
  catalog.freeze!
401
- @catalog = catalog
402
- @catalog_built = true
403
- catalog
437
+ { catalog: catalog, sigil_by_path: sigil_by_path, parse_errors_by_path: parse_errors_by_path }
404
438
  end
405
439
 
440
+ # Frozen empty bundle used when the `:catalog` producer failed (e.g. a project I/O error) so downstream
441
+ # reads see a well-formed shape rather than nil.
442
+ EMPTY_CATALOG_BUNDLE = { catalog: Catalog.new.freeze!, sigil_by_path: {}, parse_errors_by_path: {} }.freeze
443
+ private_constant :EMPTY_CATALOG_BUNDLE
444
+
406
445
  # @param root [String] directory or single file.
407
446
  # @param catalog [Catalog]
447
+ # @param sigil_by_path [Hash{String=>Symbol}] accumulator: harvested file → detected sigil level.
448
+ # @param parse_errors_by_path [Hash{String=>Array<Hash>}] accumulator: file → `{kind:,line:,column:}` tuples.
408
449
  # @param extensions [Array<String>] file extensions to accept (e.g. `[".rb"]` for project source,
409
450
  # `[".rbi"]` for Sorbet RBI tree).
410
- def harvest_path(root, catalog, extensions:)
451
+ def harvest_path(root, catalog, sigil_by_path, parse_errors_by_path, extensions)
411
452
  absolute = canonicalize(root)
412
453
  if File.directory?(absolute)
413
454
  extensions.each do |ext|
455
+ # ADR-88 WD2 — deterministic fold order matters now that the catalog VALUE is digested into the
456
+ # incremental fact-surface fingerprint: a duplicate `(class, method, kind)` sig's last-wins winner
457
+ # must not vary by machine / run. `Dir.glob` sorts its results by default on Ruby 3.0+, so the fold
458
+ # order is already stable (the slice-1 comment's "filesystem order" caveat predates that default);
459
+ # this walk relies on it rather than re-sorting.
414
460
  Dir.glob(File.join(absolute, "**", "*#{ext}")).each do |path|
415
- harvest_file(canonicalize(path), catalog)
461
+ harvest_file(canonicalize(path), catalog, sigil_by_path, parse_errors_by_path)
416
462
  end
417
463
  end
418
464
  elsif File.file?(absolute) && extensions.any? { |ext| absolute.end_with?(ext) }
419
465
  # `paths:` may list individual files (the demos do this); walk them directly rather than skipping.
420
- harvest_file(absolute, catalog)
466
+ harvest_file(absolute, catalog, sigil_by_path, parse_errors_by_path)
421
467
  end
422
468
  end
423
469
 
@@ -432,7 +478,7 @@ module Rigor
432
478
  expanded
433
479
  end
434
480
 
435
- def harvest_file(path, catalog)
481
+ def harvest_file(path, catalog, sigil_by_path, parse_errors_by_path)
436
482
  contents = io_boundary.read_file(path)
437
483
  return if contents.nil?
438
484
 
@@ -441,7 +487,7 @@ module Rigor
441
487
  # Per-call-site assertion gating consults this map at recognition time. Recorded BEFORE the
442
488
  # ignored short-circuit so a `# typed: ignore` file still reports its level to the gate (the gate
443
489
  # then chooses to suppress assertions there too — `ignore` is stricter than `false`).
444
- @sigil_by_path[path] = level
490
+ sigil_by_path[path] = level
445
491
  return if SigilDetector.ignored?(level)
446
492
 
447
493
  result = Prism.parse(contents)
@@ -460,13 +506,23 @@ module Rigor
460
506
  end
461
507
 
462
508
  errors = CatalogWalker.walk(root: result.value, catalog: sig_catalog, path: path)
463
- @parse_errors_by_path[path] = errors unless errors.empty?
509
+ # ADR-88 WD2 store Marshal-clean `{kind:, line:, column:}` tuples, not the `ParseError` (it holds a
510
+ # live Prism node the producer bundle could not serialise). `diagnostics_for_file` rebuilds the
511
+ # diagnostic from the tuple, at the same 1-based line / `start_column + 1` position `from_node` gave.
512
+ parse_errors_by_path[path] = errors.map { |error| parse_error_tuple(error) } unless errors.empty?
464
513
  rescue Plugin::AccessDeniedError, Errno::ENOENT
465
514
  # Skip files outside the trusted read scope or that vanished between glob and read; the plugin
466
515
  # produces no output for them.
467
516
  nil
468
517
  end
469
518
 
519
+ # ADR-88 WD2 — the Marshal-clean position tuple for a `CatalogWalker::ParseError`, capturing the node's
520
+ # location at harvest time so the producer bundle carries no live Prism node.
521
+ def parse_error_tuple(error)
522
+ location = error.node.location
523
+ { kind: error.kind, line: location.start_line, column: location.start_column + 1 }
524
+ end
525
+
470
526
  # Emits a `plugin.sorbet.absurd-reachable` warning for the `T.absurd(x)` call recorded in
471
527
  # `@reachable_absurd_nodes` during inference; the node rule above does the identity match and pop.
472
528
  def absurd_diagnostic(path, call_node)
@@ -535,11 +591,15 @@ module Rigor
535
591
  )
536
592
  end
537
593
 
594
+ # ADR-88 WD2 — `error` is a Marshal-clean `{kind:, line:, column:}` tuple (the pre-computed node
595
+ # location), not a Prism-node-bearing `ParseError`; build the diagnostic directly at that position (the
596
+ # `line` / `column` `from_node` would have derived).
538
597
  def parse_error_diagnostic(path, error)
539
- Rigor::Analysis::Diagnostic.from_node(
540
- error.node,
598
+ Rigor::Analysis::Diagnostic.new(
541
599
  path: path,
542
- message: parse_error_message(error.kind),
600
+ line: error[:line],
601
+ column: error[:column],
602
+ message: parse_error_message(error[:kind]),
543
603
  severity: :warning,
544
604
  rule: "parse-error"
545
605
  )
@@ -0,0 +1,9 @@
1
+ # Declares the reopening `Rigor::Source::NodeChildren` performs at load: a compiled `#rigor_each_child` is defined on
2
+ # every concrete `Prism::*Node` class (allocation-free equivalent of `compact_child_nodes.each`). Declared on the
3
+ # abstract base so every subclass resolves; the runtime definition is per-concrete-class, generated from
4
+ # `Prism::Reflection` in lib/rigor/source/node_children.rb.
5
+ module Prism
6
+ class Node
7
+ def rigor_each_child: () { (Prism::Node) -> void } -> void
8
+ end
9
+ end
@@ -3,8 +3,10 @@ module Rigor
3
3
  type closure_classification = :non_escaping | :escaping | :unknown
4
4
 
5
5
  class ExpressionTyper
6
+ def self.harvest_return_memo: () -> Hash[untyped, Array[untyped]]
6
7
  def initialize: (scope: Scope, ?tracer: FallbackTracer?) -> void
7
8
  def type_of: (untyped node) -> Type::t
9
+ def return_type_for: (untyped def_node, Type::t receiver, Array[Type::t] arg_types) -> Type::t?
8
10
  def scope: () -> Scope
9
11
  def tracer: () -> FallbackTracer?
10
12
  def dynamic_top: () -> Type::Dynamic
@@ -27,6 +29,7 @@ module Rigor
27
29
 
28
30
  def initialize: (scope: Scope, ?tracer: FallbackTracer?, ?on_enter: (^(untyped, Scope) -> void)?, ?class_context: Array[ClassFrame]) -> void
29
31
  def evaluate: (untyped node) -> [Type::t, Scope]
32
+ def content_mutated_parameter_positions: (untyped def_node) -> Array[Integer]
30
33
  def scope: () -> Scope
31
34
  def tracer: () -> FallbackTracer?
32
35
  def sub_eval: (untyped node, Scope with_scope, ?class_context: Array[ClassFrame]) -> [Type::t, Scope]
@@ -163,6 +166,10 @@ module Rigor
163
166
  def self?.record_declarations: (untyped node, Array[String] qualified_prefix, Hash[untyped, Type::t] identity_table, Hash[String, Type::t] discovered) -> void
164
167
  def self?.discovered_classes_for_paths: (Array[String] paths, ?buffer: untyped) -> Hash[String, Type::t]
165
168
  def self?.discovered_def_index_for_paths: (Array[String] paths, ?buffer: untyped) -> Hash[Symbol, untyped]
169
+ def self?.scan_summary_for_paths: (Array[String] paths, ?buffer: untyped) -> Hash[Symbol, untyped]
170
+ def self?.code_fingerprint: (String source, untyped comments) -> String
171
+ def self?.discovered_project_index_for_paths: (Array[String] paths, ?buffer: untyped) -> Hash[Symbol, untyped]
172
+ def self?.discovered_project_index_incremental: (Array[String] paths, seed_bundles: Hash[String, untyped], ?buffer: untyped) -> Hash[Symbol, untyped]
166
173
  def self?.collect_class_decls: (untyped node, Array[String] qualified_prefix, Hash[String, Type::t] accumulator) -> void
167
174
  def self?.qualified_name_for: (untyped constant_path_node) -> String?
168
175
  def self?.render_constant_path: (untyped node) -> String
@@ -28,8 +28,7 @@ class Rigor::Plugin::Base
28
28
 
29
29
  def self.narrowing_facts: (methods: Array[untyped]) { (untyped call_node, untyped scope) -> untyped } -> nil
30
30
  # Deprecating alias for `narrowing_facts` (ADR-80); removed in 0.3.0.
31
- def self.type_specifier: (methods: Array[untyped]) { (untyped call_node, untyped scope) -> untyped } -> nil
32
- def self.type_specifiers: () -> Array[untyped]
31
+ def self.narrowing_facts_rules: () -> Array[untyped]
33
32
 
34
33
  def self.suggest: (untyped name, untyped candidates) -> String?
35
34
 
@@ -48,7 +47,7 @@ class Rigor::Plugin::Base
48
47
  # Engine-executed dispatch over the declared DSLs.
49
48
  def node_rule_diagnostics: (path: untyped, scope: untyped, root: untyped) -> Array[untyped]
50
49
  def dynamic_return_type: (call_node: untyped, scope: untyped, receiver_type: untyped) -> untyped
51
- def type_specifier_facts: (call_node: untyped, scope: untyped) -> Array[untyped]
50
+ def narrowing_facts_for: (call_node: untyped, scope: untyped) -> Array[untyped]
52
51
 
53
52
  # Authoring helpers.
54
53
  def diagnostic: (untyped node, path: untyped, message: untyped, ?severity: untyped, ?rule: untyped, ?location: untyped) -> untyped
@@ -6,6 +6,7 @@ module Rigor
6
6
  def self?.nominal_for_name: (String | Symbol class_name, ?scope: Scope) -> Type::Nominal?
7
7
  def self?.singleton_for_name: (String | Symbol class_name, ?scope: Scope) -> Type::Singleton?
8
8
  def self?.constant_type_for: (String | Symbol constant_name, ?scope: Scope) -> Type::t?
9
+ def self?.resolve_constant_type: (String | Symbol name, ?scope: Scope) -> Type::t?
9
10
  def self?.instance_method_definition: (String | Symbol class_name, String | Symbol method_name, ?scope: Scope) -> untyped
10
11
  def self?.singleton_method_definition: (String | Symbol class_name, String | Symbol method_name, ?scope: Scope) -> untyped
11
12
  def self?.instance_definition: (String | Symbol class_name, ?scope: Scope) -> untyped
data/sig/rigor/scope.rbs CHANGED
@@ -11,6 +11,7 @@ module Rigor
11
11
  attr_reader dynamic_origins: Hash[untyped, Symbol]
12
12
  attr_reader local_origins: Hash[Symbol, Symbol]
13
13
  attr_reader ivar_origins: Hash[Symbol, Symbol]
14
+ attr_reader void_origins: Hash[untyped, Inference::VoidOrigin]
14
15
  attr_reader indexed_narrowings: Hash[IndexedKey, Type::t]
15
16
  attr_reader method_chain_narrowings: Hash[ChainKey, Type::t]
16
17
  attr_reader source_path: String?
@@ -28,6 +29,7 @@ module Rigor
28
29
  def discovered_def_nodes: () -> Hash[String, Hash[Symbol, untyped]]
29
30
  def discovered_singleton_def_nodes: () -> Hash[String, Hash[Symbol, untyped]]
30
31
  def discovered_def_sources: () -> Hash[String, Hash[Symbol, String]]
32
+ def discovered_singleton_def_sources: () -> Hash[String, Hash[Symbol, String]]
31
33
  def discovered_method_visibilities: () -> Hash[String, Hash[Symbol, Symbol]]
32
34
  def discovered_superclasses: () -> Hash[String, String]
33
35
  def discovered_includes: () -> Hash[String, Array[String]]
@@ -35,8 +37,11 @@ module Rigor
35
37
  def data_member_layouts: () -> Hash[String, Array[Symbol]]
36
38
  def struct_member_layouts: () -> Hash[String, { members: Array[Symbol], keyword_init: bool }]
37
39
  def param_inferred_types: () -> Hash[[String, Symbol, Symbol], Hash[Symbol, Type::t]]
40
+ def run_generation: () -> Object?
38
41
  def record_dynamic_origin: (untyped node, Symbol cause) -> Scope
39
42
 
43
+ def record_void_origin: (untyped node, Inference::VoidOrigin origin) -> Scope
44
+
40
45
  class DiscoveryIndex
41
46
  attr_reader declared_types: Hash[untyped, Type::t]
42
47
  attr_reader class_ivars: Hash[String, Hash[Symbol, Type::t]]
@@ -48,6 +53,7 @@ module Rigor
48
53
  attr_reader discovered_def_nodes: Hash[String, Hash[Symbol, untyped]]
49
54
  attr_reader discovered_singleton_def_nodes: Hash[String, Hash[Symbol, untyped]]
50
55
  attr_reader discovered_def_sources: Hash[String, Hash[Symbol, String]]
56
+ attr_reader discovered_singleton_def_sources: Hash[String, Hash[Symbol, String]]
51
57
  attr_reader discovered_method_visibilities: Hash[String, Hash[Symbol, Symbol]]
52
58
  attr_reader discovered_superclasses: Hash[String, String]
53
59
  attr_reader discovered_includes: Hash[String, Array[String]]
@@ -55,10 +61,11 @@ module Rigor
55
61
  attr_reader data_member_layouts: Hash[String, Array[Symbol]]
56
62
  attr_reader struct_member_layouts: Hash[String, { members: Array[Symbol], keyword_init: bool }]
57
63
  attr_reader param_inferred_types: Hash[[String, Symbol, Symbol], Hash[Symbol, Type::t]]
64
+ attr_reader run_generation: Object?
58
65
 
59
66
  EMPTY: DiscoveryIndex
60
67
 
61
- def with: (?declared_types: Hash[untyped, Type::t], ?class_ivars: Hash[String, Hash[Symbol, Type::t]], ?class_cvars: Hash[String, Hash[Symbol, Type::t]], ?program_globals: Hash[Symbol, Type::t], ?discovered_classes: Hash[String, Type::Singleton], ?in_source_constants: Hash[String, Type::t], ?discovered_methods: Hash[String, Hash[Symbol, Symbol]], ?discovered_def_nodes: Hash[String, Hash[Symbol, untyped]], ?discovered_singleton_def_nodes: Hash[String, Hash[Symbol, untyped]], ?discovered_def_sources: Hash[String, Hash[Symbol, String]], ?discovered_method_visibilities: Hash[String, Hash[Symbol, Symbol]], ?discovered_superclasses: Hash[String, String], ?discovered_includes: Hash[String, Array[String]], ?discovered_class_sources: Hash[String, Set[String]], ?data_member_layouts: Hash[String, Array[Symbol]], ?struct_member_layouts: Hash[String, { members: Array[Symbol], keyword_init: bool }], ?param_inferred_types: Hash[[String, Symbol, Symbol], Hash[Symbol, Type::t]]) -> DiscoveryIndex
68
+ def with: (?declared_types: Hash[untyped, Type::t], ?class_ivars: Hash[String, Hash[Symbol, Type::t]], ?class_cvars: Hash[String, Hash[Symbol, Type::t]], ?program_globals: Hash[Symbol, Type::t], ?discovered_classes: Hash[String, Type::Singleton], ?in_source_constants: Hash[String, Type::t], ?discovered_methods: Hash[String, Hash[Symbol, Symbol]], ?discovered_def_nodes: Hash[String, Hash[Symbol, untyped]], ?discovered_singleton_def_nodes: Hash[String, Hash[Symbol, untyped]], ?discovered_def_sources: Hash[String, Hash[Symbol, String]], ?discovered_singleton_def_sources: Hash[String, Hash[Symbol, String]], ?discovered_method_visibilities: Hash[String, Hash[Symbol, Symbol]], ?discovered_superclasses: Hash[String, String], ?discovered_includes: Hash[String, Array[String]], ?discovered_class_sources: Hash[String, Set[String]], ?data_member_layouts: Hash[String, Array[Symbol]], ?struct_member_layouts: Hash[String, { members: Array[Symbol], keyword_init: bool }], ?param_inferred_types: Hash[[String, Symbol, Symbol], Hash[Symbol, Type::t]], ?run_generation: Object?) -> DiscoveryIndex
62
69
  end
63
70
 
64
71
  class IndexedKey
@@ -97,6 +104,9 @@ module Rigor
97
104
  def with_declaration_sourced_local: (String | Symbol name, Type::t type) -> Scope
98
105
  def with_local_declaration_mark: (String | Symbol name) -> Scope
99
106
  def declaration_sourced?: (Symbol kind, String | Symbol name) -> bool
107
+ def with_inferred_param_mark: (String | Symbol name) -> Scope
108
+ def without_inferred_param_mark: (String | Symbol name) -> Scope
109
+ def inferred_param?: (String | Symbol name) -> bool
100
110
  def forget_match_globals: () -> Scope
101
111
  def class_ivars_for: (String | Symbol? class_name) -> Hash[Symbol, Type::t]
102
112
  def class_cvars_for: (String | Symbol? class_name) -> Hash[Symbol, Type::t]
@@ -125,6 +135,7 @@ module Rigor
125
135
  def local_facts: (String | Symbol name, ?bucket: Symbol?) -> Array[Analysis::FactStore::Fact]
126
136
  def type_of: (untyped node, ?tracer: Inference::FallbackTracer?) -> Type::t
127
137
  def evaluate: (untyped node, ?tracer: Inference::FallbackTracer?) -> [Type::t, Scope]
138
+ def user_method_return: (untyped def_node, Type::t receiver, Array[Type::t] arg_types) -> Type::t?
128
139
  def join: (Scope other) -> Scope
129
140
  def ==: (untyped other) -> bool
130
141
  alias eql? ==
@@ -141,6 +141,16 @@ The exact recipe (the `BUNDLE_GEMFILE` wiring, the Dependabot entry) is in
141
141
  role.
142
142
  - **Determinism.** Add `--no-cache` in CI if you want each run independent
143
143
  of any persisted `.rigor/cache`.
144
+ - **Cache persistence (opposite trade).** When the Rigor job's runtime
145
+ matters, persist `.rigor/cache` with the CI's cache facility. Current
146
+ Rigor detects CI and validates the restored cache by content hash
147
+ automatically (`cache.validation: auto`); on versions before the
148
+ `auto` default, **also set `RIGOR_STRICT_VALIDATION=1` on the job** —
149
+ without it a fresh checkout's regenerated stat tuples make the plugin
150
+ watch-glob cache slots read as stale on every run. A self-hosted
151
+ runner that reuses its workspace opts back into the faster stat check
152
+ with `cache.validation: stat`. Snippets: `rigor docs ci`
153
+ § "Persisting the analysis cache across runs".
144
154
 
145
155
  ## Verify
146
156
 
@@ -89,6 +89,17 @@ The manual chapter covers:
89
89
  - **Helix** (`languages.toml`)
90
90
  - **Emacs** (Eglot and lsp-mode)
91
91
 
92
+ **The editors are not equally cheap to wire.** Neovim / Helix / Emacs
93
+ are plain config snippets. **VS Code has no first-party Rigor
94
+ extension yet** — its path means authoring and side-loading a minimal
95
+ extension (a `LanguageClient` wrapper), which is a development task,
96
+ not a `settings.json` edit. Say so *before* starting: the developer
97
+ may prefer to wait for a first-class extension, and an autonomous
98
+ agent should treat the VS Code path as a **user handoff** (report the
99
+ manual's recipe; do not scaffold an unrequested extension). Phase 4's
100
+ verification (live diagnostics, hover) also requires a human in front
101
+ of the editor — an agent stops there and reports, for every editor.
102
+
92
103
  ### Phase 3 — apply the matching config
93
104
 
94
105
  Open the manual's **Editor wiring** section and apply the snippet for
@@ -21,6 +21,25 @@ frozen in this file (ADR-73).
21
21
  Work top to bottom. Stop at the first step that is not yet satisfied,
22
22
  complete it, then continue.
23
23
 
24
+ ## Autonomous / non-interactive runs
25
+
26
+ When the user has delegated the journey up front ("set this project up
27
+ and get it healthy; don't wait for me"), the skills below still contain
28
+ ask-the-user points. Handle them like this:
29
+
30
+ - A choice the user **already made in their instruction** (e.g. the
31
+ adoption mode) is settled — record it and proceed; do not re-ask.
32
+ - A low-risk, reversible, in-repo action a skill says to confirm
33
+ (installing an RBS collection, a `.gitignore` addition) is covered by
34
+ the standing delegation — do it, and note the self-answer in your
35
+ report.
36
+ - **Committing, pushing, or publishing is never covered** by a general
37
+ delegation — leave the files uncommitted and list them for the user.
38
+ - A step only a human can complete or verify (watching a CI run
39
+ execute, confirming live diagnostics inside an editor) is a
40
+ **handoff, not a stall**: do the file-level part if it is safe,
41
+ then report what remains for the user instead of waiting.
42
+
24
43
  ## Step 1 — Resolve the `rigor` command
25
44
 
26
45
  ```sh
@@ -53,9 +72,11 @@ Check the project root for `.rigor.yml` or `.rigor.dist.yml`. If
53
72
  first:
54
73
 
55
74
  ```sh
56
- rigor skill rigor-project-init
75
+ rigor skill --full rigor-project-init
57
76
  ```
58
77
 
78
+ (`rigor skill <name>` prints the skill body alone; `--full` inlines its
79
+ `references/` files too, so one call fetches the complete procedure.)
59
80
  Follow that skill's body top to bottom (it detects the stack, picks
60
81
  plugins, writes `.rigor.dist.yml`, and snapshots a baseline if needed),
61
82
  then come back to Step 4.
@@ -87,7 +108,7 @@ the `describe` output, then:
87
108
  - Then load the chosen skill and follow it:
88
109
 
89
110
  ```sh
90
- rigor skill <chosen-skill>
111
+ rigor skill --full <chosen-skill>
91
112
  ```
92
113
 
93
114
  Re-run `rigor skill describe` any time you want the next step again — it
@@ -115,8 +115,8 @@ AST walk per file — hands every matching node to the block along with a
115
115
  Optionally the plugin also declares `dynamic_return(receivers:)` /
116
116
  `narrowing_facts(methods:)` to *supply* a return type or narrowing facts
117
117
  for call sites the core analyzer types as `Dynamic`. (`narrowing_facts`
118
- was renamed from `type_specifier` in ADR-80; `type_specifier` remains as a
119
- deprecating alias removed in 0.3.0 use `narrowing_facts` in new plugins.)
118
+ was renamed from `type_specifier` in ADR-80; the old verb was removed in
119
+ 0.3.0, so `narrowing_facts` is the only spelling.)
120
120
  `#diagnostics_for_file`
121
121
  is the file-rule surface for whole-file diagnostics a per-node walk can't
122
122
  express. (`flow_contribution_for` was removed pre-1.0 in ADR-52 WD3 —
@@ -136,5 +136,5 @@ defining it now raises `ArgumentError`; use `dynamic_return` /
136
136
  | Module | Read | Covers |
137
137
  | --- | --- | --- |
138
138
  | 1 | [`references/01-plan-and-scaffold.md`](references/01-plan-and-scaffold.md) | **Phase 1.** The gem vs project-private packaging split, directory trees for both, gemspec template, project-private path-gem / `RUBYLIB` activation, the `Rigor::Plugin::Base` skeleton, `.rigor.yml` `plugins:` wiring. |
139
- | 2 | [`references/02-walker-and-types.md`](references/02-walker-and-types.md) | **Phase 2.** The `node_rule` engine-owned AST walk over Prism nodes, the `Base#diagnostic` helper, asking the analyzer for inferred types via `scope.type_of`, two-pass / lexical context (`node_file_context` / `NodeContext`), the optional `dynamic_return` / `narrowing_facts` return-type hooks (`narrowing_facts` was renamed from `type_specifier` in ADR-80, alias removed in 0.3.0; `flow_contribution_for` was removed pre-1.0 in ADR-52 WD3), calling the target library's pure methods directly rather than reimplementing them (ADR-39: `Plugin::Inflector` over the real `ActiveSupport::Inflector`; `Base.suggest` for did-you-mean), and shipping `sig/*.rbs` so the DSL's types are visible. |
139
+ | 2 | [`references/02-walker-and-types.md`](references/02-walker-and-types.md) | **Phase 2.** The `node_rule` engine-owned AST walk over Prism nodes, the `Base#diagnostic` helper, asking the analyzer for inferred types via `scope.type_of`, two-pass / lexical context (`node_file_context` / `NodeContext`), the optional `dynamic_return` / `narrowing_facts` return-type hooks (`narrowing_facts` was renamed from `type_specifier` in ADR-80, the alias removed in 0.3.0; `flow_contribution_for` was removed pre-1.0 in ADR-52 WD3), calling the target library's pure methods directly rather than reimplementing them (ADR-39: `Plugin::Inflector` over the real `ActiveSupport::Inflector`; `Base.suggest` for did-you-mean), and shipping `sig/*.rbs` so the DSL's types are visible. |
140
140
  | 3 | [`references/03-test-and-ship.md`](references/03-test-and-ship.md) | **Phase 3.** Testing a plugin from outside the monorepo — fixture projects driven through `rigor check --format json`, plus pure unit tests of dispatch tables — with RSpec or Minitest. Version pinning against the pre-1.0 contract. README. Publishing to RubyGems or keeping the plugin private. |
@@ -203,8 +203,8 @@ end
203
203
  ```
204
204
 
205
205
  > **`narrowing_facts` was renamed from `type_specifier` in ADR-80.**
206
- > `type_specifier` remains as a deprecating alias, removed in 0.3.0 — use
207
- > `narrowing_facts` in new plugins.
206
+ > The old verb was removed in 0.3.0 — `narrowing_facts` is the only
207
+ > spelling a plugin can declare.
208
208
 
209
209
  Build return types with `Rigor::Type::Combinator`:
210
210
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: rigor-plugin-review
3
3
  description: |
4
- Review an existing Rigor plugin's source against the current authoring contract and produce a prioritized upgrade path — the modernization counterpart to rigor-plugin-author. Audits config-default declaration (ADR-40), the AST-walk model (node_rule vs a hand-rolled traversal), return-type / narrowing hooks (dynamic_return / narrowing_facts, not the removed flow_contribution_for or deprecated type_specifier), the ADR-60 WD4 authoring helpers (diagnostic / diagnostics_for / suggest / producer_value / read_fact), engine-collaboration vs reimplementation, cache-producer soundness, manifest-field hygiene, and doc freshness. Triggers: "review this Rigor plugin", "does my plugin follow best practices", "upgrade our rigor-prefixed plugin to the latest contract", "modernize this plugin", "is this plugin using the current API". NOT for authoring a new plugin (use rigor-plugin-author), enabling bundled plugins on a project (use rigor-plugin-tune), or tuning plugin config.
4
+ Review an existing Rigor plugin's source against the current authoring contract and produce a prioritized upgrade path — the modernization counterpart to rigor-plugin-author. Audits config-default declaration (ADR-40), the AST-walk model (node_rule vs a hand-rolled traversal), return-type / narrowing hooks (dynamic_return / narrowing_facts, not the removed flow_contribution_for or type_specifier), the ADR-60 WD4 authoring helpers (diagnostic / diagnostics_for / suggest / producer_value / read_fact), engine-collaboration vs reimplementation, cache-producer soundness, manifest-field hygiene, and doc freshness. Triggers: "review this Rigor plugin", "does my plugin follow best practices", "upgrade our rigor-prefixed plugin to the latest contract", "modernize this plugin", "is this plugin using the current API". NOT for authoring a new plugin (use rigor-plugin-author), enabling bundled plugins on a project (use rigor-plugin-tune), or tuning plugin config.
5
5
  license: MPL-2.0
6
6
  metadata:
7
7
  version: 0.1.0
@@ -30,7 +30,7 @@ appears:
30
30
  3. **A hand-rolled AST walk** where the engine-owned `node_rule` now
31
31
  fits (ADR-37 / ADR-52).
32
32
  4. **Removed / renamed hooks** still named — `flow_contribution_for`
33
- (deleted, ADR-52 WD3) or `type_specifier` (deprecated alias for
33
+ (deleted, ADR-52 WD3) or `type_specifier` (removed in 0.3.0; renamed to
34
34
  `narrowing_facts`, ADR-80).
35
35
  5. **Stale docs** — archaeology about deleted hooks, pinned version
36
36
  references that no longer mean anything.
@@ -78,12 +78,13 @@ can.
78
78
  ## 3. Return-type & narrowing hooks (ADR-37 / ADR-52 / ADR-80) — `M`/`D`
79
79
 
80
80
  **Smell:** `flow_contribution_for` (deleted in ADR-52 WD3 — *defining it
81
- now raises `ArgumentError`*), or `type_specifier` (the pre-ADR-80 name).
81
+ now raises `ArgumentError`*), or `type_specifier` (the pre-ADR-80 name,
82
+ removed in 0.3.0 — it now raises `NoMethodError`).
82
83
 
83
84
  **Modern:** `dynamic_return(receivers:/methods:/file_methods:)` to
84
85
  *supply* a return type, `narrowing_facts(methods:)` to supply
85
- post-return narrowing facts. `type_specifier` survives as a
86
- deprecating alias removed in 0.3.0 — rename to `narrowing_facts`.
86
+ post-return narrowing facts. Rename any surviving `type_specifier` to
87
+ `narrowing_facts`.
87
88
 
88
89
  The gate resolves after `#init` when passed a callable
89
90
  (`methods: -> { [@method_name] }`), so config-derived method names work.
@@ -207,7 +208,7 @@ and put that in a CHANGELOG / migration note, not the class docstring.
207
208
  misuse. MUST be clean; fix the cause, never disable the rule.
208
209
  - `rigor plugins --strict` — the plugin still activates.
209
210
  - `rigor plugins --capabilities` — `node_rule_types` /
210
- `dynamic_return_receivers` / `type_specifier_methods` reflect the
211
+ `dynamic_return_receivers` / `narrowing_facts_methods` reflect the
211
212
  declarations.
212
213
  - The integration / unit spec — green, and byte-identical across every
213
214
  mechanical step.
@@ -17,9 +17,10 @@ and — if the project chooses it — a `.rigor-baseline.yml` snapshot.
17
17
  This skill is for **users adopting Rigor on their own project**. It
18
18
  uses the published `rigor` executable, installed standalone — Rigor
19
19
  is a tool, not a library, so it does **not** go in the project's
20
- `Gemfile`. See the manual's
21
- [Installing Rigor](../../docs/manual/01-installation.md)
22
- chapter for the install channels (`mise` recommended). This skill
20
+ `Gemfile`. For the install channels (`mise` recommended) see the
21
+ manual's Installing Rigor chapter — `rigor docs manual/01-installation`
22
+ once any `rigor` is on `PATH`, or (pre-install)
23
+ <https://github.com/rigortype/rigor/blob/master/docs/manual/01-installation.md>. This skill
23
24
  references only public CLI flags and config keys — the same surface
24
25
  `rigor --help` documents.
25
26
 
@@ -98,6 +99,29 @@ If the project's first `rigor check` reports more than ~100 errors,
98
99
  recommend acknowledge mode as the default and let the user override.
99
100
  Below that, either mode is reasonable — ask.
100
101
 
102
+ Note the ordering: the error count that feeds this recommendation (and
103
+ the `severity_profile` choice in Phase 4) is only *measured* in
104
+ Phase 6's triage run. Any profile written in Phase 4 is therefore
105
+ **provisional** — Phase 6 revisits it against the measured count (see
106
+ [`references/02-configure.md`](references/02-configure.md)
107
+ § "Severity profile").
108
+
109
+ ## Non-interactive / agent-driven runs
110
+
111
+ This skill has several ask-the-user points (the mode choice, the RBS
112
+ collection install, the final gitignore-and-commit confirmation). When
113
+ the user delegated the onboarding up front — including the adoption
114
+ mode — those points resolve without blocking:
115
+
116
+ - The **mode** the user pre-declared is settled; record it and skip the
117
+ Phase 2 presentation.
118
+ - Low-risk, reversible, in-repo confirmations (`rbs collection
119
+ install`, the `.gitignore` addition) are covered by the delegation —
120
+ act, and note each self-answer in your report.
121
+ - **Never commit or push under a general delegation.** Run the Final
122
+ step's file inventory as a *report* (what was created, what to
123
+ commit) instead of a question, and leave the commits to the user.
124
+
101
125
  ## Phase outline
102
126
 
103
127
  | Phase | What | Reference |
@@ -126,6 +150,7 @@ committed `sig/` directory.
126
150
  | 2 | [`references/02-configure.md`](references/02-configure.md) | **Phase 4.** Severity-profile choice tied to the mode. The `.rigor.dist.yml` template and every key it uses. The `.rigor.dist.yml` vs `.rigor.yml` convention. |
127
151
  | 3 | [`references/04-sig-uplift.md`](references/04-sig-uplift.md) | **Phase 5.** `rigor sig-gen --write` baseline. `rigor sig-gen --params=observed --write` attr_reader precision uplift. Handling residual `untyped` methods. Committing `sig/`. |
128
152
  | 4 | [`references/03-baseline-and-bugs.md`](references/03-baseline-and-bugs.md) | **Phases 6–8.** `rigor triage` as the diagnosis layer. Phase 6a pre-baseline cleanup loop (`pre_eval:` for monkey-patch hints, `rbs collection install`). `rigor baseline generate` + wiring `baseline:`. Surfacing likely real bugs; sig quality FP recognition (Struct `call.wrong-arity`, `-> bot` return-type-mismatch, regex-capture `$1` FPs). The two escalation paths — write a project plugin, or open a Rigor issue. |
153
+ | — (optional) | [`references/05-jit-performance.md`](references/05-jit-performance.md) | **Operational, not a phase.** Run speed via a Ruby JIT: Rigor auto-enables YJIT for long runs (~5 s break-even), how to detect JIT support in your install, the override env vars, and why YJIT beats ZJIT for Rigor on Ruby 4.0. Read only when a large project's `rigor check` wall time matters. |
129
154
 
130
155
  ## Escalation paths (Phase 7 preview)
131
156
 
@@ -201,6 +226,7 @@ only in acknowledge mode). For each, give the commit recommendation:
201
226
  | `sig/` | RBS skeletons from `rigor sig-gen` (Phase 5), if that phase ran. A first-class project artefact — it sharpens inference for everyone. | **Yes** — commit it (see [`references/04-sig-uplift.md`](references/04-sig-uplift.md) § "Commit the sig/ directory"). |
202
227
  | `.rigor/` (contains `cache/`) | The per-file analysis cache `rigor check` writes to speed up re-runs. Regenerable and machine-local. | **No** — add `.rigor/` to `.gitignore`. |
203
228
  | `.rigor.yml` | Optional per-developer local override (not written by this skill). Takes precedence over `.rigor.dist.yml`; used to opt out locally (e.g. run without the baseline). | **No** — gitignore it if a developer creates one. |
229
+ | `rbs_collection.lock.yaml` | Not a Rigor artefact — but if Phase 1 ran `rbs collection install`, the install may have regenerated this pre-existing project file (e.g. the stdlib gem list for the resolved Ruby). | **Yes**, but flag it separately: it is a change to an existing project file, not part of the Rigor file set. |
204
230
 
205
231
  Recommend the two concrete actions and **ask before doing them**
206
232
  (both touch the user's repo): (1) add `.rigor/` — and `.rigor.yml`
@@ -102,7 +102,7 @@ user as a list they can trim — do not silently enable everything.
102
102
  | Rails | `rigor-actionpack`, `rigor-activerecord`, `rigor-actionmailer`, `rigor-rails-routes`, `rigor-rails-i18n`, plus `rigor-activesupport-core-ext` (almost always needed — see below) |
103
103
  | dry-rb | `rigor-dry-types`, `rigor-dry-struct`, and `rigor-dry-schema` / `rigor-dry-validation` when those gems are present |
104
104
  | Sinatra | `rigor-sinatra` |
105
- | RSpec | `rigor-rspec` |
105
+ | RSpec | `rigor-rspec` — **only if `paths:` will include `spec/`**. The conventional scope (see "Path scope" above) excludes `spec/`, and a plugin whose rules only fire on spec files contributes nothing outside it. Skip it under the default scope; add it later if the project opts spec files into analysis. |
106
106
  | Devise / Pundit / Sidekiq present | `rigor-devise` / `rigor-pundit` / `rigor-sidekiq` |
107
107
  | Sorbet present | `rigor-sorbet` (ingests existing `sig` blocks / RBI as type sources) |
108
108
  | plain Ruby | none required — the core analyzer covers it |
@@ -37,8 +37,11 @@ automatically and emit the message described:
37
37
 
38
38
  ## Severity profile — follows the mode
39
39
 
40
- The `severity_profile:` key re-stamps every rule's severity. Set it
41
- from the Phase 2 mode:
40
+ The `severity_profile:` key sets every rule's severity and for some
41
+ rules, whether they fire at all: a rule can be *off* under `lenient`
42
+ and surface under `balanced` / `strict` (e.g. the toplevel
43
+ unresolved-call rule). Switching profiles can therefore **change the
44
+ diagnostic count**, not just the labels. Set it from the Phase 2 mode:
42
45
 
43
46
  | Mode | `severity_profile` | Why |
44
47
  | --- | --- | --- |
@@ -48,6 +51,14 @@ from the Phase 2 mode:
48
51
 
49
52
  `balanced` is the built-in default — omit the key to get it.
50
53
 
54
+ **The Phase 4 choice is provisional.** The ">100 errors" condition is
55
+ measured by Phase 6's triage, which has not run yet — so write the
56
+ best guess now and **revisit it when the triage counts land**: if the
57
+ measured `error` count is small, prefer `balanced` even for a large
58
+ acknowledge-mode app. Make any profile change **before** generating
59
+ the baseline (Phase 7), and re-run `rigor triage` after changing it —
60
+ the count the baseline snapshots must come from the final profile.
61
+
51
62
  ## No separate installation needed
52
63
 
53
64
  All plugins ship **bundled inside the `rigortype` gem**. The
@@ -57,9 +68,10 @@ the gem's own load path. No Gemfile entry, no `bundle install`,
57
68
  no separate gem channel.
58
69
 
59
70
  **The `rigortype` gem itself stays out of the project's
60
- `Gemfile`** — install it standalone per the manual's
61
- [Installing Rigor](../../docs/manual/01-installation.md) chapter
62
- (`mise use gem:rigortype` is the recommended channel). The
71
+ `Gemfile`** — install it standalone per the manual's Installing
72
+ Rigor chapter (`rigor docs manual/01-installation`, or pre-install
73
+ <https://github.com/rigortype/rigor/blob/master/docs/manual/01-installation.md>;
74
+ `mise use gem:rigortype` is the recommended channel). The
63
75
  project's Gemfile is untouched by this workflow.
64
76
 
65
77
  ## The template
@@ -191,6 +203,28 @@ shape):
191
203
  rigor plugins --strict
192
204
  ```
193
205
 
206
+ ### `rigor plugins` verifies activation, not analysis-time health
207
+
208
+ A plugin that activates cleanly can still degrade **during
209
+ analysis** — e.g. its model-index / routes-parsing stage fails on
210
+ this machine — and that failure surfaces only as a
211
+ `plugin.<id>.load-error` **warning inside the diagnostic stream**,
212
+ easy to miss among hundreds of lines. After the first full
213
+ `rigor check` / `rigor triage` run (Phase 6), check for it
214
+ explicitly:
215
+
216
+ ```sh
217
+ rigor check --format json | jq '[.diagnostics[] | select(.rule | endswith("load-error"))]'
218
+ ```
219
+
220
+ Any hit means that plugin contributed **no diagnostics and no
221
+ types** for the capability that failed — reduced coverage, silently.
222
+ Surface it to the user rather than letting it slide into the
223
+ baseline: a `load-error` bucket in `.rigor-baseline.yml` would hide
224
+ the degradation permanently. If the error reproduces across runs,
225
+ report it upstream (<https://github.com/rigortype/rigor/issues>)
226
+ with the message text.
227
+
194
228
  ## Output of this module
195
229
 
196
230
  A committed `.rigor.dist.yml` with `paths:`, `exclude:`,
@@ -14,6 +14,14 @@ the triage command instead:
14
14
  rigor triage --format json
15
15
  ```
16
16
 
17
+ > **Parsing the JSON from a script**: diagnostic messages can carry
18
+ > non-ASCII bytes from the project's own strings (i18n content,
19
+ > comments), which makes a naive `File.read` + `JSON.parse` raise
20
+ > `Encoding::InvalidByteSequenceError`. Force UTF-8 and scrub —
21
+ > `JSON.parse(File.read(f, encoding: "UTF-8").scrub)` — or use `jq`,
22
+ > which handles it natively. The same applies to
23
+ > `rigor check --format json`.
24
+
17
25
  `rigor triage` runs the same analysis as `rigor check`, then returns
18
26
  a structured summary instead of the per-line dump. It is read-only
19
27
  and advisory — it never edits config and never writes a baseline.
@@ -79,6 +87,18 @@ If triage flags `activesupport-core-ext` (or any config gap),
79
87
  baseline and the real-bug review should both run against the
80
88
  post-config diagnostic set, not the inflated one.
81
89
 
90
+ ### Check for analysis-time plugin failures before baselining
91
+
92
+ Scan the `distribution` for any `plugin.<id>.load-error` rule (or run
93
+ the `jq` one-liner in
94
+ [`02-configure.md`](02-configure.md) § "`rigor plugins` verifies
95
+ activation, not analysis-time health"). A `load-error` means the
96
+ plugin activated but one of its capabilities failed while analysing
97
+ *this* project — its diagnostics and types are silently absent from
98
+ the stream you are about to snapshot. Surface it to the user and
99
+ resolve (or at least record) it **before** Phase 7: a `load-error`
100
+ bucket baked into the baseline hides the degradation permanently.
101
+
82
102
  ## Phase 6a — Pre-baseline cleanup
83
103
 
84
104
  Before generating the baseline, **apply every quick fix that triage