rigortype 0.4.0 → 0.4.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 (237) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/08-understanding-errors.md +3 -2
  25. data/docs/handbook/11-sig-gen.md +6 -0
  26. data/docs/handbook/appendix-liskov.md +5 -0
  27. data/docs/install.md +87 -15
  28. data/docs/manual/02-cli-reference.md +2 -2
  29. data/docs/manual/03-configuration.md +6 -1
  30. data/docs/manual/04-diagnostics.md +2 -1
  31. data/docs/manual/06-baseline.md +2 -0
  32. data/docs/manual/07-plugins.md +1 -1
  33. data/docs/manual/08-skills.md +4 -3
  34. data/docs/manual/12-caching.md +43 -4
  35. data/docs/manual/14-rails-quickstart.md +19 -11
  36. data/docs/manual/18-removing-dead-code.md +32 -2
  37. data/docs/manual/plugins/README.md +6 -1
  38. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  39. data/docs/manual/plugins/rigor-alba.md +60 -0
  40. data/docs/manual/plugins/rigor-minitest.md +22 -4
  41. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  42. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  43. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  44. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  45. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +184 -61
  47. data/lib/rigor/analysis/check_rules.rb +407 -144
  48. data/lib/rigor/analysis/dependency_recorder.rb +215 -18
  49. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  50. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  51. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  52. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  53. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  54. data/lib/rigor/analysis/incremental.rb +29 -9
  55. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +291 -0
  56. data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
  57. data/lib/rigor/analysis/incremental_session.rb +287 -50
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  59. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  60. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +43 -9
  64. data/lib/rigor/analysis/run_stats.rb +2 -2
  65. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
  68. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  69. data/lib/rigor/analysis/runner.rb +371 -49
  70. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  71. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  72. data/lib/rigor/analysis/template_unit_paths.rb +6 -4
  73. data/lib/rigor/analysis/template_units.rb +15 -5
  74. data/lib/rigor/analysis/worker_session.rb +11 -7
  75. data/lib/rigor/cache/descriptor.rb +40 -1
  76. data/lib/rigor/cache/file_digest.rb +28 -0
  77. data/lib/rigor/cache/incremental_snapshot.rb +92 -8
  78. data/lib/rigor/cache/store.rb +63 -3
  79. data/lib/rigor/cache/value_digest.rb +325 -0
  80. data/lib/rigor/cli/annotate_command.rb +5 -0
  81. data/lib/rigor/cli/baseline_command.rb +7 -18
  82. data/lib/rigor/cli/check_command.rb +85 -14
  83. data/lib/rigor/cli/doctor_command.rb +1 -1
  84. data/lib/rigor/cli/effects_report.rb +2 -2
  85. data/lib/rigor/cli/trace_command.rb +5 -0
  86. data/lib/rigor/cli/type_of_command.rb +5 -3
  87. data/lib/rigor/cli/unused_command.rb +16 -10
  88. data/lib/rigor/cli.rb +11 -3
  89. data/lib/rigor/config_audit.rb +3 -2
  90. data/lib/rigor/config_path.rb +29 -0
  91. data/lib/rigor/configuration/severity_profile.rb +11 -0
  92. data/lib/rigor/configuration.rb +52 -7
  93. data/lib/rigor/effects/plugin_facts.rb +6 -6
  94. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  95. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  96. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  97. data/lib/rigor/environment/lockfile_resolver.rb +10 -1
  98. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  99. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  100. data/lib/rigor/environment/rbs_loader.rb +284 -15
  101. data/lib/rigor/environment/required_features.rb +206 -0
  102. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  103. data/lib/rigor/environment.rb +34 -73
  104. data/lib/rigor/inference/acceptance.rb +28 -3
  105. data/lib/rigor/inference/block_parameter_binder.rb +29 -13
  106. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  107. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  108. data/lib/rigor/inference/captured_locals.rb +7 -1
  109. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  110. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  111. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  112. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  113. data/lib/rigor/inference/declaration_walk.rb +7 -0
  114. data/lib/rigor/inference/def_node_resolver.rb +26 -3
  115. data/lib/rigor/inference/definer_resolution.rb +266 -0
  116. data/lib/rigor/inference/expression_typer.rb +351 -61
  117. data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
  118. data/lib/rigor/inference/global_write_census.rb +7 -4
  119. data/lib/rigor/inference/guard_rebinding.rb +54 -13
  120. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  121. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  122. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  123. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  124. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  125. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  126. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  127. data/lib/rigor/inference/macro_block_self_type.rb +73 -14
  128. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  129. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  130. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  131. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  132. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  133. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  134. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  135. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  136. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +305 -54
  137. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  138. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  139. data/lib/rigor/inference/module_function_state.rb +214 -0
  140. data/lib/rigor/inference/multi_target_binder.rb +21 -16
  141. data/lib/rigor/inference/narrowing.rb +166 -22
  142. data/lib/rigor/inference/object_mixins.rb +180 -0
  143. data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
  144. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  145. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  146. data/lib/rigor/inference/refine_census.rb +239 -0
  147. data/lib/rigor/inference/refine_self.rb +317 -0
  148. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  149. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  150. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  151. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  152. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  153. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  154. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  155. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +143 -0
  156. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  157. data/lib/rigor/inference/scope_indexer.rb +2129 -354
  158. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  159. data/lib/rigor/inference/statement_evaluator.rb +149 -32
  160. data/lib/rigor/inference/string_mutation.rb +9 -3
  161. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  162. data/lib/rigor/inference/version_guard.rb +19 -10
  163. data/lib/rigor/language_server/project_context.rb +31 -0
  164. data/lib/rigor/language_server/server.rb +18 -0
  165. data/lib/rigor/plugin/base.rb +28 -8
  166. data/lib/rigor/plugin/inflector.rb +1 -1
  167. data/lib/rigor/plugin/io_boundary.rb +124 -7
  168. data/lib/rigor/plugin/isolation.rb +1 -1
  169. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  170. data/lib/rigor/plugin/trust_policy.rb +3 -3
  171. data/lib/rigor/project_environment.rb +29 -6
  172. data/lib/rigor/protection/discovery_seed.rb +18 -9
  173. data/lib/rigor/protection/mutation_cache.rb +4 -4
  174. data/lib/rigor/protection/mutator.rb +1 -1
  175. data/lib/rigor/reflection/constant_ancestors.rb +211 -46
  176. data/lib/rigor/reflection/constant_path.rb +33 -7
  177. data/lib/rigor/reflection.rb +41 -10
  178. data/lib/rigor/scope/discovery_index.rb +258 -3
  179. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  180. data/lib/rigor/scope/resolution_chain.rb +1032 -0
  181. data/lib/rigor/scope.rb +480 -328
  182. data/lib/rigor/sig_gen/alias_index.rb +1 -1
  183. data/lib/rigor/sig_gen/classification.rb +4 -1
  184. data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
  185. data/lib/rigor/sig_gen/generator.rb +77 -21
  186. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  187. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  188. data/lib/rigor/source/alias_names.rb +52 -0
  189. data/lib/rigor/source/literals.rb +3 -2
  190. data/lib/rigor/source/symbol_name.rb +20 -0
  191. data/lib/rigor/type/combinator.rb +59 -3
  192. data/lib/rigor/type/float_range.rb +4 -1
  193. data/lib/rigor/type/nominal.rb +6 -1
  194. data/lib/rigor/version.rb +1 -1
  195. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  196. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  197. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  198. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  199. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  200. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  201. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  202. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  203. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  204. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  205. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  206. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  207. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  208. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  209. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  210. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  211. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  212. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  213. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  214. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  215. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  216. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  217. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +13 -2
  218. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  219. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  220. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  221. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  222. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  223. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  224. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  225. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  226. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  227. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  228. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  229. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  230. data/sig/rigor/environment.rbs +2 -0
  231. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  232. data/sig/rigor/scope.rbs +69 -4
  233. data/sig/rigor/source.rbs +4 -0
  234. data/sig/rigor.rbs +3 -1
  235. data/skills/rigor-project-init/SKILL.md +37 -11
  236. data/skills/rigor-project-init/references/02-configure.md +7 -2
  237. metadata +90 -1
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Analysis
5
+ # The `RIGOR_SHADOW_RULE_WALK` switch, read in one place by both halves of the shadow harness: the rule
6
+ # walk's collectors (ADR-53 Track B, {CheckRules}) and the discovery tables (ADR-116 WD5,
7
+ # {Inference::DeclarationWalk::Shadow}, which also says how a run must be made), and by the cache keys that
8
+ # must not let a warm run answer for either. A leaf file, so the ADR-87 boot-slim probe loads it without
9
+ # any engine code.
10
+ module ShadowHarness
11
+ ENV_KEY = "RIGOR_SHADOW_RULE_WALK"
12
+
13
+ module_function
14
+
15
+ # Set to anything, `0` and the empty string included.
16
+ def enabled?
17
+ !ENV[ENV_KEY].nil?
18
+ end
19
+
20
+ # What the run-result cache and the incremental snapshot key on: nil with the harness off, which adds
21
+ # no key slot, so a run without the variable computes the key it computed before the harness existed.
22
+ def cache_identity
23
+ enabled? ? ENV_KEY : nil
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Rigor
6
+ module Analysis
7
+ # Issue #1536 (ADR-89 WD1 amendment) — what the incremental session knows about the RBS that source-RBS
8
+ # synthesizers derive from each file's comments. The declaration gates read a file's Ruby side only; a
9
+ # synthesizer's output reaches the environment as a `virtual:` buffer, and a reader of that RBS records no
10
+ # edge back to the file, so the session asks this gate one question before deciding a closure: did the
11
+ # edit move any synthesized output? A yes re-analyses the whole project.
12
+ #
13
+ # Each seed bundle carries its file's {Environment::SourceRbsSynthesis.digest} under {DIGEST_KEY}. The
14
+ # gate writes it ({#stamp}), not the scope indexer that builds the rest of the bundle: the digest needs a
15
+ # plugin registry, which discovery never sees. A stamp must describe the synthesized RBS the cached
16
+ # diagnostics of the file's readers were computed under; where that cannot be vouched for, it is nil, which
17
+ # reads as moved.
18
+ #
19
+ # The synthesizers are read from a registry the gate loads for itself, WITHOUT `#prepare`, because the
20
+ # closure is decided before the recheck runner (which loads and prepares its own) exists. That is exact
21
+ # only for a synthesizer reachable without `#prepare`, so {#verify} compares the two sets after every run:
22
+ # a plugin that builds its synthesizer in `#prepare` leaves the gate UNTRUSTED for the rest of the
23
+ # session — every stored digest reads as unknown, and every edit re-analyses the whole project.
24
+ class SourceRbsGate
25
+ DIGEST_KEY = :source_rbs_digest
26
+
27
+ # The paths {#stamp} last stored as unknown because the bytes its bundle was built from are not the
28
+ # bytes the closure was decided on. The session forgets their recorded content digests, so the next run
29
+ # detects them as changed and, reading the unknown stamp, re-analyses the project.
30
+ attr_reader :unbound
31
+
32
+ def initialize(configuration:, cache_store:, plugin_requirer:)
33
+ @configuration = configuration
34
+ @cache_store = cache_store
35
+ @plugin_requirer = plugin_requirer
36
+ @synthesizers = nil
37
+ @readings = nil
38
+ @unbound = Set.new.freeze
39
+ @untrusted = false
40
+ end
41
+
42
+ # True once a run's prepared registry declared a different synthesizer set from the one the gate
43
+ # digests with. Sticky for the session: the configuration, and so the plugin set, is fixed for its life.
44
+ def untrusted?
45
+ @untrusted
46
+ end
47
+
48
+ # Whether this edit moved what any synthesizer contributes: a changed file whose digest differs from
49
+ # the one on its snapshot bundle (or either is unknown), an added file that contributes anything, or a
50
+ # removed file that did. Decided from the synthesizers' output, never from which plugins `plugins:`
51
+ # names. The readings taken here are kept for {#stamp}.
52
+ def moved?(bundles, changed, added, removed)
53
+ return true if @untrusted && [changed, added, removed].any?(&:any?)
54
+
55
+ @readings = (changed + added).to_h { |path| [path, reading(path)] }
56
+ none = Environment::SourceRbsSynthesis::NO_CONTRIBUTION
57
+ changed.any? { |path| !unmoved?(bundles, path) } ||
58
+ added.any? { |path| @readings.fetch(path).last != none } ||
59
+ removed.any? { |path| stored(bundles, path) != none }
60
+ end
61
+
62
+ # Stamps every bundle that needs a digest: one the runner built this run (a baseline's all, a recheck's
63
+ # re-walked ones), and one carrying nil that this run can now vouch for. A bundle the runner reused keeps
64
+ # the digest it carries, which stays exact because a bundle is reused only for byte-identical content.
65
+ #
66
+ # A digest is vouched for only by a reading of the bytes the bundle was built from: the reading
67
+ # {#moved?} took when the closure was decided, or, when `whole_project` (every file was just
68
+ # re-analysed), one taken now. Anything else is stamped nil and listed in {#unbound}; a file saved after
69
+ # the closure was decided is never re-read, because its readers' cached answers predate that save. An
70
+ # untrusted gate stamps every bundle unknown.
71
+ def stamp(bundles, whole_project: false)
72
+ readings = @readings || {}
73
+ @readings = nil
74
+ @unbound = Set.new
75
+ return distrust(bundles) if @untrusted
76
+
77
+ targets = bundles.select { |path, bundle| stamp?(bundle, whole_project || readings.key?(path)) }
78
+ return bundles if targets.empty?
79
+
80
+ stamped = bundles.dup
81
+ targets.each do |path, bundle|
82
+ stamped[path] = bundle.merge(DIGEST_KEY => bound_digest(path, bundle, readings[path], whole_project))
83
+ end
84
+ stamped
85
+ end
86
+
87
+ # Compares, by plugin id and order, the synthesizers the gate digests with against those of
88
+ # `prepared_registry` — the registry the run's environment was built from, after `#prepare` (nil when
89
+ # it could not be built, which also distrusts). Runs after every run, a run that digested nothing
90
+ # included: a stamp written by an earlier process from the other set would otherwise be trusted. On a
91
+ # mismatch the gate turns untrusted and every bundle is stamped unknown. Returns the bundles to keep.
92
+ def verify(prepared_registry, bundles)
93
+ return bundles if synthesizer_ids(prepared_registry&.source_rbs_synthesizers) == synthesizer_ids(synthesizers)
94
+
95
+ @untrusted = true
96
+ distrust(bundles)
97
+ end
98
+
99
+ private
100
+
101
+ def unmoved?(bundles, path)
102
+ before = stored(bundles, path)
103
+ !before.nil? && before == @readings.fetch(path).last
104
+ end
105
+
106
+ def stored(bundles, path)
107
+ bundles[path]&.fetch(DIGEST_KEY, nil)
108
+ end
109
+
110
+ def distrust(bundles)
111
+ bundles.transform_values { |bundle| bundle.merge(DIGEST_KEY => nil) }
112
+ end
113
+
114
+ # A bundle without a digest always needs one; one carrying nil is re-stamped only when this run can
115
+ # vouch for it, so an unknown stamp left by a distrust or an unbound read clears on the file's next edit
116
+ # or the next whole-project run.
117
+ def stamp?(bundle, vouched)
118
+ !bundle.key?(DIGEST_KEY) || (bundle[DIGEST_KEY].nil? && vouched)
119
+ end
120
+
121
+ # The digest for `bundle` when a reading vouches for the bytes it was built from, else nil (and the path
122
+ # joins {#unbound}).
123
+ def bound_digest(path, bundle, reading, whole_project)
124
+ reading ||= reading(path) if whole_project
125
+ return reading.last if reading && reading.first == bundle[:digest]
126
+
127
+ @unbound << path
128
+ nil
129
+ end
130
+
131
+ # `[content SHA-256, digest]`, the SHA nil unless the file read the same before and after the digest was
132
+ # taken, so the pair can be matched against a bundle's content digest.
133
+ def reading(path)
134
+ before = content_sha(path)
135
+ digest = Environment::SourceRbsSynthesis.digest(synthesizers, path, @cache_store)
136
+ [before && before == content_sha(path) ? before : nil, digest]
137
+ end
138
+
139
+ def content_sha(path)
140
+ Digest::SHA256.file(path).hexdigest
141
+ rescue SystemCallError
142
+ nil
143
+ end
144
+
145
+ def synthesizers
146
+ @synthesizers ||= Runner::ProjectPrePasses.new(
147
+ configuration: @configuration, cache_store: @cache_store, buffer: nil,
148
+ plugin_requirer: @plugin_requirer, pool_mode: -> { false }
149
+ ).prepared_registry(prepare: false).source_rbs_synthesizers
150
+ end
151
+
152
+ def synthesizer_ids(pairs)
153
+ pairs&.map { |plugin, _| plugin.manifest.id.to_s }
154
+ end
155
+ end
156
+ end
157
+ end
@@ -21,13 +21,15 @@ module Rigor
21
21
  # how an LSP publish for an open `.rbx` reported `call.unresolved-toplevel` for every helper while the
22
22
  # unit sat in the index under its relative name). A path outside the root is left alone.
23
23
  def relative(path, root)
24
- # `expand_path` against the root first, so `./app/views/x.rbx`, `app/views/x.rbx` and
24
+ # `absolute_path` against the root first, so `./app/views/x.rbx`, `app/views/x.rbx` and
25
25
  # `$ROOT/lib/../app/views/x.rbx` all reduce to the one absolute spelling before anything is
26
26
  # compared. Without it an `--instead-of=./app/views/x.rbx` matched no unit and was parsed as plain
27
27
  # Ruby — the spellings a shell and an editor produce are not the spelling `Dir.glob` returns.
28
- root_dir = File.expand_path(root.to_s)
29
- text = File.expand_path(path.to_s, root_dir)
30
- # BOTH spellings of the root. `expand_path` does not resolve symlinks, and the root is not always
28
+ # `absolute_path`, not `expand_path`: a leading `~` is a directory name here (`~drafts/a.rb`), and
29
+ # `expand_path` reads it as a user's home and raises `ArgumentError` for a user that does not exist.
30
+ root_dir = File.absolute_path(root.to_s)
31
+ text = File.absolute_path(path.to_s, root_dir)
32
+ # BOTH spellings of the root. `absolute_path` does not resolve symlinks, and the root is not always
31
33
  # already resolved: `Dir.pwd` is (which is why this was latent), but a caller may pass any path.
32
34
  prefixes(root_dir).each do |prefix|
33
35
  return text.delete_prefix(prefix) if text.start_with?(prefix)
@@ -190,11 +190,11 @@ module Rigor
190
190
  end
191
191
 
192
192
  def [](path)
193
- @entries[normalize(path)]
193
+ entry_for(path)
194
194
  end
195
195
 
196
196
  def key?(path)
197
- @entries.key?(normalize(path))
197
+ !entry_for(path).nil?
198
198
  end
199
199
 
200
200
  # `{ path => ruby_source }` — what {Runner#parse_source} and {WorkerSession#parse_source} read
@@ -287,7 +287,7 @@ module Rigor
287
287
  # it compiles a partial's locals into the method's parameters, and it is what makes `locals:` mean
288
288
  # anything.
289
289
  def parse_scopes(path)
290
- entry = @entries[normalize(path)]
290
+ entry = entry_for(path)
291
291
  return nil if entry.nil? || entry.locals.empty?
292
292
 
293
293
  [entry.locals.keys.map(&:to_sym)]
@@ -301,7 +301,7 @@ module Rigor
301
301
  end
302
302
 
303
303
  def unit_key_for(path)
304
- @entries[normalize(path)]&.unit_key
304
+ entry_for(path)&.unit_key
305
305
  end
306
306
 
307
307
  # Binds the declared `self`, locals and ivar seeds onto the per-file scope, so the unit's body types
@@ -315,7 +315,7 @@ module Rigor
315
315
  # the honest reading of "a receiver is declared and the analyzer cannot see it" (ADR-5), and it is
316
316
  # silent.
317
317
  def seed(scope, path)
318
- entry = @entries[normalize(path)]
318
+ entry = entry_for(path)
319
319
  return scope if entry.nil?
320
320
 
321
321
  scope = bind_self(scope, entry)
@@ -369,6 +369,16 @@ module Rigor
369
369
 
370
370
  private
371
371
 
372
+ # The unit `path` names, or nil. A run with no units answers before reducing the path: the reduction
373
+ # realpaths the root on every call, and every analysed file is looked up at least twice, which made 922
374
+ # `File.realpath` calls on a template-free `rigor check` of the v0.3.9 `lib`
375
+ # ([#1507](https://github.com/rigortype/rigor/issues/1507)).
376
+ def entry_for(path)
377
+ return nil if @entries.empty?
378
+
379
+ @entries[normalize(path)]
380
+ end
381
+
372
382
  def normalize(path)
373
383
  TemplateUnitPaths.relative(path, @root)
374
384
  end
@@ -6,6 +6,7 @@ require_relative "../environment"
6
6
  require_relative "../project_environment"
7
7
  require_relative "../scope"
8
8
  require_relative "../cache/store"
9
+ require_relative "shadow_harness"
9
10
  require_relative "../plugin"
10
11
  require_relative "../rbs_extended/reporter"
11
12
  require_relative "../reflection"
@@ -85,7 +86,7 @@ module Rigor
85
86
  plugin_blueprints: [], explain: false, buffer: nil,
86
87
  synthetic_method_index: nil, project_patched_methods: nil,
87
88
  project_scope_seed: {}, source_files: [], record_dependencies: false,
88
- template_units: nil, locked_gems: nil)
89
+ template_units: nil, locked_gems: nil, required_features: nil)
89
90
  @configuration = configuration
90
91
  @cache_store = cache_store
91
92
  @explain = explain
@@ -118,7 +119,9 @@ module Rigor
118
119
  # RubyGems module state a non-main Ractor may not touch, so a Ractor worker that resolved it here got
119
120
  # `Ractor::IsolationError` and an empty map — no gem `sig/` directories, no ADR-72 overlays. nil (the
120
121
  # fork backend and every other caller) resolves inside `Environment.for_project` exactly as before.
121
- @locked_gems = locked_gems
122
+ # Issue #1700 — `required_features` is the coordinator's scan ({Runner::PoolCoordinator#required_features_for});
123
+ # nil scans `source_files` inside `Environment.for_project`.
124
+ @environment_inputs = { locked_gems: locked_gems, required_features: required_features }.freeze
122
125
 
123
126
  # NOTE: `Inference::MethodDispatcher::FileFolding.fold_platform_specific_paths` is process-global
124
127
  # state. Writing it from a non-main Ractor would raise `Ractor::IsolationError`, so the session does
@@ -152,7 +155,7 @@ module Rigor
152
155
  synthetic_method_index: @synthetic_method_index,
153
156
  project_patched_methods: @project_patched_methods,
154
157
  source_files: @source_files,
155
- locked_gems: @locked_gems
158
+ **@environment_inputs
156
159
  )
157
160
  @prepare_diagnostics = run_plugin_prepare.freeze
158
161
  end
@@ -235,7 +238,8 @@ module Rigor
235
238
  scope_index: index,
236
239
  comments: parse_result.comments,
237
240
  disabled_rules: @configuration.disabled_rules,
238
- node_collectors: node_collectors
241
+ node_collectors: node_collectors,
242
+ stated_runtime_ruby: @configuration.stated_runtime_ruby
239
243
  )
240
244
  diagnostics += plugin_emitted_diagnostics(path, root, scope, node_results)
241
245
  diagnostics + explain_diagnostics(path, parse_result.value, scope)
@@ -364,12 +368,12 @@ module Rigor
364
368
  def build_trust_policy
365
369
  trusted_gems = @configuration.plugins.map { |entry| trusted_gem_name(entry) }.uniq
366
370
  roots = [Dir.pwd]
367
- Array(@configuration.signature_paths).each { |sp| roots << File.expand_path(sp) }
371
+ Array(@configuration.signature_paths).each { |sp| roots << File.absolute_path(sp) }
368
372
  trusted_gems.each do |gem_name|
369
373
  path = trusted_gem_root(gem_name)
370
374
  roots << path if path
371
375
  end
372
- @configuration.plugins_io_allowed_paths.each { |p| roots << File.expand_path(p) }
376
+ @configuration.plugins_io_allowed_paths.each { |p| roots << File.absolute_path(p) }
373
377
 
374
378
  Plugin::TrustPolicy.new(
375
379
  trusted_gems: trusted_gems,
@@ -440,7 +444,7 @@ module Rigor
440
444
  results = walk.diagnostics_for_file(
441
445
  path: path, scope: scope, root: root, collector_driver: driver
442
446
  )
443
- if ENV["RIGOR_SHADOW_RULE_WALK"]
447
+ if ShadowHarness.enabled?
444
448
  CheckRules.shadow_verify_converged_collectors(path, root, scope_index, node_collectors)
445
449
  end
446
450
  results.each_with_object({}.compare_by_identity) do |result, by_plugin|
@@ -58,7 +58,31 @@ module Rigor
58
58
  # searches ahead of a class's own `def`s. A pre-10 bundle would silently contribute no prepends for an
59
59
  # unchanged file, so that file's consumers would keep answering with the pre-fix MRO — cached bundles
60
60
  # must read as misses once and rebuild carrying the slot.
61
- SCHEMA_VERSION = 10
61
+ # v11: #1558 — a plugin producer served from its own cache now replays its entry's dependency rows into
62
+ # the plugin's IoBoundary, so the run-result entry and the entry of any producer computed after it carry
63
+ # the served value's inputs. A pre-11 entry written while a producer hit lacks those rows and would
64
+ # validate fresh across exactly the edit they exist to catch. A checkout's source digest already moved
65
+ # every such key, and a release clears the root through `Store::PAYLOAD_ABI_VERSION`; the bump closes
66
+ # the remaining window, a same-`VERSION` build installed as `gems/rigortype-<VERSION>`, which
67
+ # {EngineSource} treats as version-pinned (#285) and so keys on the version alone.
68
+ # v12: the def-index seed bundles gain the `:unpositioned_mixins` table, the mixin edges whose order is
69
+ # not a fact, which an ancestor-order reader declines on. A pre-12 bundle would silently contribute no
70
+ # such edge for an unchanged file, so cached bundles must read as misses once and rebuild carrying it.
71
+ # v13/v14: #1587 — the prepend table keeps every statement (a repeated `prepend` is no longer collapsed at
72
+ # the freeze) and a module written to both the include and the prepend table, or a repeated `extend`, is
73
+ # named unpositioned. A pre-14 bundle would hold the collapsed prepend list and miss the taint for an
74
+ # unchanged file, so cached bundles must read as misses once and rebuild.
75
+ # v15: #1592 — an `extend` (or a `class << self` include) written inside a block the walk used to skip is
76
+ # now recorded and named unpositioned. A pre-15 bundle would hold neither for an unchanged file.
77
+ # v16: ADR-119 WD1 — the def-index seed bundles gain the `:siblings` Hash (the `possible_*` / `contested_*`
78
+ # halves of the discovery pairs). A pre-16 bundle would contribute no sibling for an unchanged file, so cached
79
+ # bundles must read as misses once and rebuild carrying it.
80
+ # v17: ADR-119 WD3 — the def-contribution producers fill the siblings (a conditional `def`'s name possible, its
81
+ # def-node slot and envelope contested). A pre-17 bundle carries the empty siblings of the walk before them, so
82
+ # an unchanged file would fold as "no possible fact" where a cold walk records one.
83
+ # v18: ADR-119 WD3 — the visibility walk fills `contested_discovered_method_visibilities`; a pre-18 bundle
84
+ # carries it empty, so an unchanged file's conditional visibility would fold as certain.
85
+ SCHEMA_VERSION = 18
62
86
 
63
87
  # Per-slot entry value objects. Constructors validate enums / required fields and freeze the resulting
64
88
  # struct so no caller can mutate after the entry is in a Descriptor.
@@ -105,6 +129,21 @@ module Rigor
105
129
  new(path: path, comparator: :stat, value: packed)
106
130
  end
107
131
 
132
+ # A `:stat` row whose recording instant is no later than `limit_ns`: any file whose recorded mtime is at or
133
+ # after `limit_ns` is then racy, and validation re-hashes it instead of trusting its stat tuple. Any other
134
+ # row, and one whose instant is already earlier, is returned as is.
135
+ def with_recording_instant_at_most(limit_ns)
136
+ return self unless comparator == :stat
137
+
138
+ parts = value.split
139
+ return self unless parts.size == 6 && Integer(parts[5], 10) > limit_ns
140
+
141
+ parts[5] = limit_ns.to_s
142
+ self.class.new(path: path, comparator: comparator, value: parts.join(" "))
143
+ rescue ArgumentError
144
+ self
145
+ end
146
+
108
147
  # ADR-45 WD1 (#577) — the absence row: fresh while `path` does not exist, stale the moment anything
109
148
  # (a file, a directory, a symlink) comes into existence there. {Plugin::IoBoundary#read_file}
110
149
  # records one when a read fails because the path is missing, so a value computed on "X is absent"
@@ -112,6 +112,34 @@ module Rigor
112
112
  hexdigest(path) == digest
113
113
  end
114
114
 
115
+ # ADR-45 WD2 (#1507) — `packed` re-packed against `path`'s current stat, for a writer about to record it
116
+ # again: when the tuple moved (a `touch`, a checkout) or the entry is racy, but the file still hashes to the
117
+ # recorded digest, the new pack carries the current tuple and this run's recording instant, so the next
118
+ # validation is one `File::Stat` again rather than a re-hash every time.
119
+ #
120
+ # The hash is taken fresh, never from the per-run memo: a memoised digest describes the bytes as they were
121
+ # when change detection read them, and pairing it with a later stat would vouch for bytes written since. And
122
+ # the re-pack is kept only when the tuple it packed, taken after the hash, is the one taken before it: a
123
+ # write landing while the file is hashed moves the change time, and leaves `packed` as it was.
124
+ #
125
+ # `packed` comes back unchanged when its tuple still matches and it is not racy, and whenever the bytes no
126
+ # longer match the digest: a row that must stay stale is never refreshed into a fresh one. nil when `packed`
127
+ # is not a stat pack. A stat failure (the file is gone) returns `packed` unchanged, which then validates as
128
+ # stale.
129
+ def self.refresh_stat(path, packed)
130
+ parsed = parse_stat(packed)
131
+ return nil if parsed.nil?
132
+
133
+ before = File.stat(path)
134
+ return packed if !racy?(parsed) && tuple_matches?(before, parsed)
135
+ return packed unless Digest::SHA256.file(path).hexdigest == parsed[0]
136
+
137
+ repacked = pack_stat(path, parsed[0])
138
+ repacked && tuple_matches?(before, parse_stat(repacked)) ? repacked : packed
139
+ rescue SystemCallError
140
+ packed
141
+ end
142
+
115
143
  # The content-digest field of a packed `:stat` entry ({.pack_stat}); nil when the entry is absent or not
116
144
  # well-formed. Exposed for the caller that must compare the recorded digest against bytes it hashes
117
145
  # itself rather than against the file on disk — the incremental session's editor-mode freshness check,
@@ -5,6 +5,11 @@ require "digest"
5
5
  require "zlib"
6
6
 
7
7
  require_relative "engine_source"
8
+ require_relative "store"
9
+ require_relative "../analysis/shadow_harness"
10
+ require_relative "../analysis/run_cache_key"
11
+ require_relative "../environment"
12
+ require_relative "../analysis/path_expansion"
8
13
 
9
14
  module Rigor
10
15
  module Cache
@@ -138,7 +143,33 @@ module Rigor
138
143
  # the `write` / `to_str` / `to_int` / hatch definitions and top-level mixins it holds, which the `global.*`
139
144
  # write rules decline on project-wide. A pre-29 bundle would fold as "none of those", and a warm run would
140
145
  # report a write a cold run exempts.
141
- SCHEMA = 29
146
+ # 30: issue #1536 gives each seed bundle a `source_rbs_digest` — the digest of what every loaded source-RBS
147
+ # synthesizer contributes for the file — which ADR-89 WD1 now compares beside `declaration_signature`
148
+ # in place of switching the gate off whenever `rigor-rbs-inline` is configured. A pre-30 bundle
149
+ # carries none, which the session reads as "unknown" and so as moved: sound, but every first edit to
150
+ # each file would re-analyse the whole project, so the gate rejects the blob and the next run is cold.
151
+ # 31: issue #1574 digests `plugin_fact_digest` and a seed bundle's `source_rbs_digest` with
152
+ # {ValueDigest} instead of over `Marshal.dump` bytes, so the same value digests alike whether a producer
153
+ # computed it or its cache entry served it. A pre-31 digest is in the old form and would never match, so
154
+ # the next run would go cold anyway; the bump makes that a clean miss instead of a "plugin fact surface
155
+ # changed" note that nothing changed.
156
+ # 32: each seed bundle gains an `unpositioned_mixins` table — the file's mixin edges whose order is not a
157
+ # fact (`Scope::DiscoveryIndex#unpositioned_mixins`), which an ancestor-order reader declines on. A pre-32
158
+ # bundle would fold as "every edge positioned", and a warm run would answer an order a cold run declines.
159
+ # 33: the mixin producers change what a seed bundle holds (#1587 — the prepend table keeps every statement,
160
+ # and a both-kind module or a repeated `extend` is named unpositioned), so a pre-33 bundle folds as the
161
+ # collapsed, positioned table and a warm run would answer an order a cold run declines.
162
+ # 34: #1592 — an `extend` or a `class << self` include inside a block the extends walk used to skip is now
163
+ # recorded and named unpositioned, so a pre-34 bundle folds as if the edge did not exist.
164
+ # 35: ADR-119 WD1 — each seed bundle gains a `siblings` Hash, the `possible_*` / `contested_*` siblings of the
165
+ # `Scope::DiscoveryIndex` members that may admit a possible fact. Every sibling is empty until a producer
166
+ # fills one, so a pre-35 bundle folds as "no possible fact", which is what it would hold today; the bump keeps
167
+ # that true once one does.
168
+ # 36: ADR-119 WD3 — the def-contribution producers fill those siblings, so a pre-36 bundle folds an unchanged
169
+ # file's conditional `def` as certain and a warm run would answer a definer a cold run declines.
170
+ # 37: ADR-119 WD3 — the visibility walk fills `contested_discovered_method_visibilities`, which a pre-37 bundle
171
+ # carries empty.
172
+ SCHEMA = 37
142
173
 
143
174
  # The persisted per-file state.
144
175
  # `cache` maps an analyzed file to its diagnostics.
@@ -210,8 +241,13 @@ module Rigor
210
241
  # rebuild — the engine version + schema, the engine's own SOURCE when the version does not pin it, the
211
242
  # resolved configuration, the analysis **roots** (the path arguments, e.g. `["lib"]`, NOT the expanded
212
243
  # file list — so a snapshot is keyed to an invocation's roots but adding / removing a file under them is
213
- # handled incrementally by the session, not a full rebuild), the resolved gem set (`Gemfile.lock` /
214
- # `rbs_collection`), and the project's own RBS (`signature_paths` file contents). Built WITHOUT
244
+ # handled incrementally by the session, not a full rebuild), the resolved gem set (the lockfiles
245
+ # {Analysis::RunCacheKey.resolved_lockfile_paths} resolves — `bundler.lockfile:` / `rbs_collection.lockfile:`,
246
+ # else `./Gemfile.lock` / `./rbs_collection.lock.yaml` — the same files the run-result key digests), the
247
+ # project's own RBS (the `sig/` roots the environment actually loads: the explicit
248
+ # `signature_paths`, or the auto-detected `<root>/sig` when `signature_paths` is nil), and the contents of
249
+ # every `pre_eval:` file (its PATH is in the configuration, but an edit to a file outside the analysed
250
+ # paths changes no analysed file, so it needs its own part). Built WITHOUT
215
251
  # constructing the RBS environment so the warm path can gate the load cheaply, before the costly env
216
252
  # build. The `--verify-incremental` gate is the safety net for any under-capture (it would surface as an
217
253
  # incremental-vs-full mismatch). Returns nil on any error → the caller falls back to a non-persisted run.
@@ -225,16 +261,32 @@ module Rigor
225
261
  # part, so a released gem's fingerprint is byte-identical to the pre-#289 one and its warm snapshots
226
262
  # survive the upgrade untouched.
227
263
  def self.fingerprint(configuration:, roots:)
264
+ bundler_lock, collection_lock = Analysis::RunCacheKey.resolved_lockfile_paths(configuration)
228
265
  parts = [
229
266
  "engine:#{Rigor::VERSION}:#{SCHEMA}",
230
267
  "config:#{Digest::SHA256.hexdigest(Marshal.dump(configuration.to_h))}",
231
268
  "roots:#{Array(roots).map(&:to_s).sort.join("\n")}",
232
- "gems:#{digest_file_if_present('Gemfile.lock')}",
233
- "rbs_collection:#{digest_file_if_present('rbs_collection.lock.yaml')}",
234
- "sig:#{digest_signature_paths(configuration.signature_paths)}"
269
+ "gems:#{digest_file_if_present(bundler_lock)}",
270
+ "rbs_collection:#{digest_file_if_present(collection_lock)}",
271
+ "sig:#{digest_signature_paths(project_sig_roots(configuration))}",
272
+ "pre_eval:#{digest_pre_eval(configuration.pre_eval)}"
235
273
  ]
274
+ parts.concat(dependency_source_parts(configuration))
236
275
  identity = EngineSource.process_identity
237
276
  parts << "engine-source:#{identity}" if identity
277
+ # Issue #1700 — the gated vendored signatures the run's source activates. A file that gains or drops a
278
+ # `require "prime"` changes the environment every other file was analysed against, which no per-file
279
+ # dependency edge records, so the whole snapshot goes. Scanned over the configured paths plus the roots'
280
+ # files, the set the run's environment scans; no part when nothing is required, so every other
281
+ # project's fingerprint is as it was.
282
+ features = Environment::RequiredFeatures.for_configuration(
283
+ configuration, Analysis::PathExpansion.ruby_files(roots, configuration.exclude_patterns)
284
+ )
285
+ parts << "required-features:#{features.join(',')}" unless features.empty?
286
+ # ADR-116 WD5 — the snapshot's per-file rows and seed bundles are only as checked as the run that
287
+ # wrote them ({Inference::DeclarationWalk::Shadow}); nil with the harness off adds no part.
288
+ shadow = Analysis::ShadowHarness.cache_identity
289
+ parts << "shadow-harness:#{shadow}" if shadow
238
290
  Digest::SHA256.hexdigest(parts.join("\x00"))
239
291
  rescue StandardError
240
292
  # {EngineSource::Unavailable} lands here too, and nil is the answer it requires rather than one it
@@ -245,11 +297,41 @@ module Rigor
245
297
  nil
246
298
  end
247
299
 
300
+ # ADR-10 slice 3, issue #1814 — the walked gems' versions: their method catalogue, their refinement table and the
301
+ # constants they bind reach every file's checks, and the lockfile digest above does not cover a project with none.
302
+ # No part for a project that lists no gem, so its fingerprint is what it was.
303
+ def self.dependency_source_parts(configuration)
304
+ rows = Analysis::RunCacheKey.dependency_source_entries(configuration).map do |row|
305
+ [row.gem_name, row.gem_version, row.mode]
306
+ end
307
+ rows.empty? ? [] : ["dependency-source:#{rows.sort.inspect}"]
308
+ end
309
+ private_class_method :dependency_source_parts
310
+
248
311
  def self.digest_file_if_present(path)
249
- File.file?(path) ? Digest::SHA256.file(path).hexdigest : "absent"
312
+ path = path&.to_s
313
+ path && File.file?(path) ? Digest::SHA256.file(path).hexdigest : "absent"
250
314
  end
251
315
  private_class_method :digest_file_if_present
252
316
 
317
+ # Content-digest every `pre_eval:` file in configuration order, so an edit to one invalidates the snapshot
318
+ # even when it lies outside the analysed paths. A missing file digests as "absent", so creating it counts too.
319
+ def self.digest_pre_eval(paths)
320
+ digest = Digest::SHA256.new
321
+ Array(paths).each do |path|
322
+ digest << path.to_s << "\0" << digest_file_if_present(path.to_s) << "\0"
323
+ end
324
+ digest.hexdigest
325
+ end
326
+ private_class_method :digest_pre_eval
327
+
328
+ # The `sig/` roots the environment loads for this configuration: the configured `signature_paths`, or the
329
+ # auto-detected `<root>/sig` when those are nil ({Environment.project_signature_roots}).
330
+ def self.project_sig_roots(configuration)
331
+ Environment.project_signature_roots(configuration.signature_paths).map(&:to_s)
332
+ end
333
+ private_class_method :project_sig_roots
334
+
253
335
  # Content-digest every `.rbs` under the configured signature paths (sorted for determinism) so a project
254
336
  # RBS edit invalidates the snapshot. Sig trees are small; content (not mtime) keeps it stable across
255
337
  # checkouts.
@@ -351,7 +433,9 @@ module Rigor
351
433
  effects_identity: payload.effects_identity,
352
434
  run_level_rows: payload.run_level_rows
353
435
  )
354
- blob = Zlib::Deflate.deflate(raw)
436
+ # #1507 — every `--incremental` run that analysed anything rewrites this whole blob, so its deflate is
437
+ # on the edit path; see {Store::DEFLATE_LEVEL}.
438
+ blob = Zlib::Deflate.deflate(raw, Store::DEFLATE_LEVEL)
355
439
  tmp = "#{@path}.#{Process.pid}.tmp"
356
440
  File.binwrite(tmp, blob)
357
441
  File.rename(tmp, @path)