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
@@ -24,8 +24,22 @@ module Rigor
24
24
  # Bump when the on-disk shape changes so stale snapshots are ignored rather than mis-deserialized. 5:
25
25
  # the blob is zlib-deflated (ADR-54 WD2 parity with `Store` entries — the snapshot is the one cache
26
26
  # artefact that does not go through `Store`); a raw pre-5 blob fails the inflate and loads as nil, the
27
- # usual fault-tolerant cold-run path.
28
- SCHEMA = 5
27
+ # usual fault-tolerant cold-run path. 6: adds the ADR-85 WD2 `seed_bundles` section (per-file discovery
28
+ # contributions with `(node_id, name, fingerprint)` def-node handles); a pre-6 blob mismatches the
29
+ # SCHEMA gate and loads as nil (a clean cold rebuild — no migration). 7: the seed bundle gains a
30
+ # `singleton_def_sources` table (ADR-46 slice 4 extended to class/singleton methods) AND `digests`
31
+ # switches to ADR-87 packed stat entries; a pre-7 blob mismatches the gate and loads as nil (clean cold
32
+ # rebuild). 8: each seed bundle additionally gains a comment-stripped `code_fingerprint` for the B1
33
+ # bundle-equality gate; a pre-8 blob mismatches and loads as nil (clean cold rebuild). 9: adds the ADR-88
34
+ # WD1 `plugin_fact_digest` (a fingerprint of the plugin fact SURFACE — ADR-9 facts, ADR-60 producer
35
+ # values, and `incremental_state_fingerprint` hooks — that a cached diagnostic can depend on but the
36
+ # global fingerprint does not capture); a pre-9 blob mismatches the SCHEMA gate and loads as nil (a clean
37
+ # cold rebuild — no migration). 10: ADR-89 WD1 adds a per-file `declaration_signature` to each seed
38
+ # bundle (the per-def parameter-shape / visibility / ancestry surface the declaration-stability gate
39
+ # compares) and WD2 adds `return_summaries` (per-def observed-key return descriptors + mutation-effect
40
+ # sets the behavioural-stability gate compares); a pre-10 blob mismatches the SCHEMA gate and loads as
41
+ # nil (a clean cold rebuild — no migration).
42
+ SCHEMA = 10
29
43
 
30
44
  # The persisted per-file state.
31
45
  # `cache` maps an analyzed file to its diagnostics.
@@ -39,9 +53,25 @@ module Rigor
39
53
  # ADR-46 slice 3:
40
54
  # `missing` maps a consumer to Set<"kind:name"> it looked up and missed.
41
55
  # `class_decls` maps a path to Set<qualified class name> it declares.
56
+ # ADR-85 WD2:
57
+ # `seed_bundles` maps an analyzed path to its per-file discovery contribution (plain-data tables +
58
+ # `(node_id, name, fingerprint)` def-node handles + content digest), so a warm recheck rebuilds the
59
+ # cross-file index by folding bundles instead of parsing every file.
60
+ # ADR-88 WD1:
61
+ # `plugin_fact_digest` is a SHA-256 hex fingerprint of the plugin fact surface at the run that wrote the
62
+ # snapshot (or nil for a plugin-free project). A warm recheck recomputes it and, on a mismatch, discards
63
+ # the snapshot and runs a full analysis — the guard for a plugin sig/catalog edit that the global
64
+ # fingerprint cannot see.
65
+ # ADR-89 WD2:
66
+ # `return_summaries` maps a `[path, "Class#method" | "Class.method"]` to the callee's persisted
67
+ # behavioural surface `{ keys:, returns:, effects: }` — the observed `[receiver, arg_types]` call keys
68
+ # (Marshal-clean type tuples), their `describe(:short)` return descriptors, and the content-mutated
69
+ # parameter positions. A recheck re-evaluates a declaration-stable changed callee at these keys and,
70
+ # when every return + the effects are unchanged, skips its symbol dependents.
42
71
  Payload = Data.define(:cache, :sources, :digests, :analyzed,
43
72
  :symbol_sources, :ancestry_sources, :symbol_fingerprints,
44
- :missing, :class_decls)
73
+ :missing, :class_decls, :seed_bundles, :plugin_fact_digest,
74
+ :return_summaries)
45
75
 
46
76
  # The global fingerprint that gates a snapshot load: a digest of the inputs whose change requires a full
47
77
  # rebuild — the engine version + schema, the resolved configuration, the analysis **roots** (the path
@@ -104,7 +134,10 @@ module Rigor
104
134
  ancestry_sources: data[:ancestry_sources] || {},
105
135
  symbol_fingerprints: data[:symbol_fingerprints] || {},
106
136
  missing: data[:missing] || {},
107
- class_decls: data[:class_decls] || {}
137
+ class_decls: data[:class_decls] || {},
138
+ seed_bundles: data[:seed_bundles] || {},
139
+ plugin_fact_digest: data[:plugin_fact_digest],
140
+ return_summaries: data[:return_summaries] || {}
108
141
  )
109
142
  rescue StandardError
110
143
  nil
@@ -122,7 +155,10 @@ module Rigor
122
155
  ancestry_sources: payload.ancestry_sources,
123
156
  symbol_fingerprints: payload.symbol_fingerprints,
124
157
  missing: payload.missing,
125
- class_decls: payload.class_decls
158
+ class_decls: payload.class_decls,
159
+ seed_bundles: payload.seed_bundles,
160
+ plugin_fact_digest: payload.plugin_fact_digest,
161
+ return_summaries: payload.return_summaries
126
162
  )
127
163
  blob = Zlib::Deflate.deflate(raw)
128
164
  tmp = "#{@path}.#{Process.pid}.tmp"
@@ -3,6 +3,7 @@
3
3
  require "digest"
4
4
 
5
5
  require_relative "descriptor"
6
+ require_relative "file_digest"
6
7
 
7
8
  module Rigor
8
9
  module Cache
@@ -17,15 +18,36 @@ module Rigor
17
18
  Descriptor.new(
18
19
  gems: [rbs_gem_entry],
19
20
  files: file_entries(loader),
20
- configs: [libraries_entry(loader), virtual_rbs_entry(loader)].compact
21
+ configs: config_entries(loader)
21
22
  )
22
23
  end
23
24
 
25
+ # Lazy-files variant for the ADR-45 run-diagnostics record-and-validate cache. The cache KEY reads only
26
+ # `gems` + `configs` ({Runner#run_key_descriptor}); the RBS signature-tree `files` are read solely on a
27
+ # MISS, by the dependency descriptor ({Runner#run_dependency_descriptor}). So a warm HIT never digests
28
+ # the (large, vendored) RBS tree. {RunDescriptor} is NOT a {Descriptor} — it is never composed, hashed,
29
+ # or `==`'d, only its three readers are consulted — so deferring `files` costs no soundness, and `gems`
30
+ # / `configs` are byte-identical to {.build} (the key is unchanged).
31
+ def self.build_run(loader)
32
+ RunDescriptor.new(loader: loader, gems: [rbs_gem_entry], configs: config_entries(loader))
33
+ end
34
+
35
+ # The `gems` + `configs` slots the run cache key reads. Cheap — no RBS env load, no file digesting
36
+ # (only the configured library names + any plugin-synthesised virtual RBS are hashed).
37
+ def self.config_entries(loader)
38
+ [libraries_entry(loader.libraries), virtual_rbs_entry(loader)].compact
39
+ end
40
+
41
+ # Public (ADR-87 WD4) so the boot-slimming run-cache probe reconstructs the identical `gems` +
42
+ # `rbs.libraries` key slots the runner writes, from the config-derived library list, without a loader.
24
43
  def self.rbs_gem_entry
25
44
  Descriptor::GemEntry.new(name: "rbs", requirement: ">= 0", locked: ::RBS::VERSION.to_s)
26
45
  end
27
46
 
28
- def self.file_entries(loader)
47
+ # @param comparator [Symbol] `:digest` (default) for the env-cache KEY descriptor ({.build}), where the
48
+ # value must be deterministic; `:stat` (ADR-87 WD1) for the validation-only run-dependency descriptor
49
+ # ({RunDescriptor#files}), where the stat tier short-circuits the SHA-256 on an unmoved file.
50
+ def self.file_entries(loader, comparator: :digest)
29
51
  roots = loader.signature_paths +
30
52
  Rigor::Environment::RbsLoader.vendored_gem_sig_paths +
31
53
  Rigor::Environment::RbsLoader.core_overlay_sig_paths
@@ -33,17 +55,20 @@ module Rigor
33
55
  next [] unless root.directory?
34
56
 
35
57
  Dir.glob(root.join("**", "*.rbs")).map do |path|
36
- Descriptor::FileEntry.new(
37
- path: path,
38
- comparator: :digest,
39
- value: Digest::SHA256.file(path).hexdigest
40
- )
58
+ digest = FileDigest.hexdigest(path)
59
+ if comparator == :stat
60
+ Descriptor::FileEntry.stat(path: path, digest: digest)
61
+ else
62
+ Descriptor::FileEntry.new(path: path, comparator: :digest, value: digest)
63
+ end
41
64
  end
42
65
  end
43
66
  end
44
67
 
45
- def self.libraries_entry(loader)
46
- sorted = loader.libraries.map(&:to_s).sort
68
+ # @param library_names [Array<String, Symbol>] the loader's merged library list (or, on the WD4 probe
69
+ # path, `Environment::DEFAULT_LIBRARIES + config.libraries` reconstructed without a loader).
70
+ def self.libraries_entry(library_names)
71
+ sorted = library_names.map(&:to_s).sort
47
72
  Descriptor::ConfigEntry.new(
48
73
  key: "rbs.libraries",
49
74
  value_hash: Digest::SHA256.hexdigest(sorted.join("\n"))
@@ -66,7 +91,28 @@ module Rigor
66
91
  )
67
92
  end
68
93
 
69
- private_class_method :rbs_gem_entry, :file_entries, :libraries_entry, :virtual_rbs_entry
94
+ private_class_method :virtual_rbs_entry
95
+
96
+ # The lazy-files run descriptor {RbsDescriptor.build_run} returns. Exposes the three readers the
97
+ # run-diagnostics cache consults — `gems` + `configs` are supplied eagerly (they feed the cache KEY,
98
+ # and are cheap); `files` (the RBS signature-tree digests, read only on a MISS) is computed once on
99
+ # first access and memoised, so a warm HIT never pays for it.
100
+ class RunDescriptor
101
+ attr_reader :gems, :configs
102
+
103
+ def initialize(loader:, gems:, configs:)
104
+ @loader = loader
105
+ @gems = gems
106
+ @configs = configs
107
+ end
108
+
109
+ def files
110
+ # ADR-87 WD1 — this descriptor is validated (never a cache key), so the RBS signature tree rides the
111
+ # stat-then-digest `:stat` tier: a warm run stat-checks the (large, vendored) tree instead of
112
+ # re-hashing it.
113
+ @files ||= RbsDescriptor.file_entries(@loader, comparator: :stat)
114
+ end
115
+ end
70
116
  end
71
117
  end
72
118
  end
@@ -224,10 +224,9 @@ module Rigor
224
224
  key = key_descriptor.cache_key_for(producer_id: producer_id, params: params)
225
225
  path = disk ? entry_path(producer_id, key) : nil
226
226
  cached = path && read_entry(path, deserialize: deserialize)
227
- if cached && (pair = cached.value).is_a?(Array) && pair.size == 2 &&
228
- pair[1].is_a?(Descriptor) && pair[1].fresh?
227
+ if (validated = fresh_pair_value(cached))
229
228
  @monitor.synchronize { record(:hits, producer_id) }
230
- return pair[0]
229
+ return validated[0]
231
230
  end
232
231
 
233
232
  value, dependency_descriptor = block_given? ? yield : [nil, Descriptor.new]
@@ -239,6 +238,24 @@ module Rigor
239
238
  value
240
239
  end
241
240
 
241
+ # ADR-87 WD4 — the READ half of {#fetch_or_validate} with no compute and no write: returns the cached
242
+ # value on a fresh hit, nil on a miss / stale / unavailable-disk. The boot-slimming hit probe calls this
243
+ # to serve a run's diagnostics WITHOUT loading the inference engine — it never runs a producer block, so
244
+ # there is nothing to write. Records a hit (for `--cache-stats` parity) but never a miss (a probe miss
245
+ # hands off to the full path, which records its own).
246
+ def peek_validated(producer_id:, key_descriptor:, params: {}, deserialize: nil)
247
+ validate_producer_id!(producer_id)
248
+ return nil unless ensure_schema_version!
249
+
250
+ key = key_descriptor.cache_key_for(producer_id: producer_id, params: params)
251
+ cached = read_entry(entry_path(producer_id, key), deserialize: deserialize)
252
+ validated = fresh_pair_value(cached)
253
+ return nil if validated.nil?
254
+
255
+ @monitor.synchronize { record(:hits, producer_id) }
256
+ validated[0]
257
+ end
258
+
242
259
  # ADR-6 § "Eviction" — compaction pass over the on-disk cache. No-op when the store is read-only. Stale
243
260
  # temp file cleanup and the whole-project generation cap run regardless of `max_bytes:` — they reclaim
244
261
  # provably-dead bytes (leaked temp files, unreachable content-keyed generations) rather than enforcing a
@@ -274,6 +291,19 @@ module Rigor
274
291
  Entry = Data.define(:descriptor_bytes, :value)
275
292
  private_constant :Entry
276
293
 
294
+ # A record-and-validate entry is a fresh hit iff it deserialised to a `[value, dependency_descriptor]`
295
+ # pair whose descriptor still validates against the filesystem. Returns the pair on a fresh hit, nil
296
+ # otherwise. Shared by {#fetch_or_validate} and {#peek_validated} so both apply identical hit criteria.
297
+ def fresh_pair_value(cached)
298
+ return nil if cached.nil?
299
+
300
+ pair = cached.value
301
+ return nil unless pair.is_a?(Array) && pair.size == 2 &&
302
+ pair[1].is_a?(Descriptor) && pair[1].fresh?
303
+
304
+ pair
305
+ end
306
+
277
307
  def record(counter, producer_id)
278
308
  case counter
279
309
  when :hits then @hits += 1
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ # Runtime CI-environment detection (ADR-51 WD7), modelled on `OndraM/ci-detector` (the library PHPStan uses). Reads
5
+ # the well-known environment variables a CI provider sets and returns the matching {Platform}, classifying it into a
6
+ # **tier** that decides how `rigor check` surfaces diagnostics there:
7
+ #
8
+ # :native_stdout — Rigor has a native format that renders purely from
9
+ # stdout, so it is auto-emitted on top of the human
10
+ # output (GitHub Actions → `github`, TeamCity →
11
+ # `teamcity`). These are the first-class platforms.
12
+ # :native_artifact — Rigor has a native format but it needs a CI-wired
13
+ # report artifact, not stdout (GitLab CI → `gitlab`).
14
+ # First-class, but Rigor only *hints* the format.
15
+ # :reviewdog — no native Rigor format; second-class, routed through
16
+ # reviewdog (`checkstyle`/`sarif`) or `junit`. Hint
17
+ # only.
18
+ #
19
+ # Lives outside the CLI namespace because it is a pure function of the environment hash with no CLI dependency, and
20
+ # non-CLI consumers exist: {Configuration#cache_validation_strict?} resolves the `auto` cache-validation default
21
+ # through it (a fresh CI checkout regenerates every stat tuple, so ADR-87's stat tier cannot work there).
22
+ # Detection is fully testable; callers pass `ENV`.
23
+ # `RIGOR_CI_DETECT=0` (or `false`/`no`) disables it globally — the seam the spec suite uses for determinism.
24
+ module CiDetector
25
+ Platform = Struct.new(:id, :name, :format, :tier, keyword_init: true) do
26
+ def native_stdout? = tier == :native_stdout
27
+ def native_artifact? = tier == :native_artifact
28
+ def reviewdog? = tier == :reviewdog
29
+ end
30
+
31
+ # The detection table, ordered most-specific first so the generic `CI=true` catch-all is last (a provider that
32
+ # also sets `CI` is still recognised by its own variable). `match` is `:truthy` (value in 1/true/yes/on),
33
+ # `:present` (variable set non-empty), or `:equals`.
34
+ PROVIDERS = [
35
+ { id: "github-actions", name: "GitHub Actions", format: "github", tier: :native_stdout,
36
+ var: "GITHUB_ACTIONS", match: :truthy },
37
+ { id: "gitlab", name: "GitLab CI", format: "gitlab", tier: :native_artifact,
38
+ var: "GITLAB_CI", match: :truthy },
39
+ { id: "teamcity", name: "TeamCity", format: "teamcity", tier: :native_stdout,
40
+ var: "TEAMCITY_VERSION", match: :present },
41
+ { id: "circleci", name: "CircleCI", format: nil, tier: :reviewdog,
42
+ var: "CIRCLECI", match: :truthy },
43
+ { id: "jenkins", name: "Jenkins", format: nil, tier: :reviewdog,
44
+ var: "JENKINS_URL", match: :present },
45
+ { id: "travis", name: "Travis CI", format: nil, tier: :reviewdog,
46
+ var: "TRAVIS", match: :truthy },
47
+ { id: "appveyor", name: "AppVeyor", format: nil, tier: :reviewdog,
48
+ var: "APPVEYOR", match: :truthy },
49
+ { id: "azure-pipelines", name: "Azure Pipelines", format: nil, tier: :reviewdog,
50
+ var: "TF_BUILD", match: :present },
51
+ { id: "bitbucket", name: "Bitbucket Pipelines", format: nil, tier: :reviewdog,
52
+ var: "BITBUCKET_BUILD_NUMBER", match: :present },
53
+ { id: "buildkite", name: "Buildkite", format: nil, tier: :reviewdog,
54
+ var: "BUILDKITE", match: :truthy },
55
+ { id: "drone", name: "Drone CI", format: nil, tier: :reviewdog,
56
+ var: "DRONE", match: :truthy },
57
+ { id: "semaphore", name: "Semaphore", format: nil, tier: :reviewdog,
58
+ var: "SEMAPHORE", match: :truthy },
59
+ { id: "codeship", name: "Codeship", format: nil, tier: :reviewdog,
60
+ var: "CI_NAME", match: :equals, value: "codeship" },
61
+ { id: "ci", name: "CI", format: nil, tier: :reviewdog,
62
+ var: "CI", match: :truthy }
63
+ ].freeze
64
+
65
+ module_function
66
+
67
+ # Returns the detected {Platform}, or nil when no CI is recognised or detection is disabled via `RIGOR_CI_DETECT`.
68
+ def detect(env = ENV)
69
+ return nil if disabled?(env)
70
+
71
+ row = PROVIDERS.find { |provider| matches?(env, provider) }
72
+ return nil if row.nil?
73
+
74
+ Platform.new(id: row[:id], name: row[:name], format: row[:format], tier: row[:tier])
75
+ end
76
+
77
+ def disabled?(env)
78
+ %w[0 false no off].include?(env["RIGOR_CI_DETECT"].to_s.strip.downcase)
79
+ end
80
+
81
+ def matches?(env, provider)
82
+ value = env[provider[:var]].to_s.strip
83
+ case provider[:match]
84
+ when :truthy then %w[1 true yes on].include?(value.downcase)
85
+ when :present then !value.empty?
86
+ when :equals then value.downcase == provider[:value]
87
+ end
88
+ end
89
+ end
90
+ end
@@ -9,11 +9,13 @@ require_relative "../configuration"
9
9
  require_relative "options"
10
10
  require_relative "../environment"
11
11
  require_relative "../scope"
12
+ require_relative "../source/node_children"
12
13
  require_relative "../inference/def_return_typer"
13
14
  require_relative "../inference/scope_indexer"
14
15
  require_relative "../inference/statement_evaluator"
15
16
  require_relative "prism_colorizer"
16
17
  require_relative "command"
18
+ require_relative "probe_environment"
17
19
 
18
20
  module Rigor
19
21
  class CLI
@@ -105,7 +107,7 @@ module Rigor
105
107
  # bindings, so a loop-body line annotates the joined widened type (`Integer`) rather than a stale
106
108
  # first-iterations constant (`1 | 2`).
107
109
  scope_index = Inference::ScopeIndexer.index(
108
- parse_result.value, default_scope: base_scope(configuration),
110
+ parse_result.value, default_scope: base_scope(configuration, file),
109
111
  converged_loop_recording: true
110
112
  )
111
113
  line_types = LineTypeCollector.new(scope_index).collect(parse_result.value)
@@ -128,12 +130,12 @@ module Rigor
128
130
  @out.puts(JSON.generate({ "annotations" => annotations }))
129
131
  end
130
132
 
131
- def base_scope(configuration)
133
+ # The plugin-aware environment for the annotated file, so a `#=> <type>` matches what `rigor check`
134
+ # infers on that line — including types synthesized from the file's own inline RBS annotations (the file
135
+ # is threaded as the synthesizer's `source_files:`; see {ProbeEnvironment}).
136
+ def base_scope(configuration, file)
132
137
  Scope.empty(
133
- environment: Environment.for_project(
134
- libraries: configuration.libraries,
135
- signature_paths: configuration.signature_paths
136
- )
138
+ environment: ProbeEnvironment.build(configuration: configuration, source_files: [file])
137
139
  )
138
140
  end
139
141
 
@@ -247,14 +249,18 @@ module Rigor
247
249
  block.call(stmt)
248
250
  end
249
251
  else
250
- node.compact_child_nodes.each { |child| each_statement(child, &block) }
252
+ node.rigor_each_child { |child| each_statement(child, &block) }
251
253
  end
252
254
  end
253
255
 
254
256
  # For a line no statement closes (the `if` / block header lines), fall back to the widest expression ending there.
257
+ # A hash-pair node (`1 => 2,` inside a multi-line literal / keyword-argument list) is not an expression — typing
258
+ # it only echoes the `Dynamic[top]` fallback — so those interior lines stay bare and the literal's type appears
259
+ # once, on the line the enclosing statement closes.
255
260
  def fill_uncovered_lines(program, by_line)
256
261
  widest_per_line(program).each do |line, node|
257
262
  next if by_line.key?(line)
263
+ next if node.is_a?(Prism::AssocNode) || node.is_a?(Prism::AssocSplatNode)
258
264
 
259
265
  type = node.is_a?(Prism::BlockParametersNode) ? block_params_type(node) : type_of(node)
260
266
  by_line[line] = type unless type.nil?
@@ -298,7 +304,7 @@ module Rigor
298
304
  return if node.nil?
299
305
 
300
306
  block.call(node)
301
- node.compact_child_nodes.each { |child| walk(child, &block) }
307
+ node.rigor_each_child { |child| walk(child, &block) }
302
308
  end
303
309
 
304
310
  # Types the node through the flow evaluator (not the bare expression typer) under its recorded entry scope, so
@@ -331,7 +337,7 @@ module Rigor
331
337
  return if node.nil?
332
338
 
333
339
  block.call(node) if node.is_a?(Prism::DefNode)
334
- node.compact_child_nodes.each { |child| each_def_node(child, &block) }
340
+ node.rigor_each_child { |child| each_def_node(child, &block) }
335
341
  end
336
342
  end
337
343
  end
@@ -87,8 +87,11 @@ module Rigor
87
87
  "match-mode: #{options.fetch(:match_mode)})"
88
88
  )
89
89
  if configuration.baseline_path.nil?
90
+ # Name the config file that actually governs this project (`.rigor.dist.yml` when no local override
91
+ # exists), so the hint points at the file the user must edit.
92
+ config_name = options.fetch(:config) || Configuration.discover || ".rigor.yml"
90
93
  @err.puts(
91
- "rigor: note — `.rigor.yml` does not declare `baseline:`; " \
94
+ "rigor: note — `#{config_name}` does not declare `baseline:`; " \
92
95
  "add `baseline: #{path}` to activate the suppression."
93
96
  )
94
97
  end