rigortype 0.4.3 → 0.4.5

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 (172) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/data/core_overlay/ostruct.rbs +40 -0
  4. data/docs/handbook/11-sig-gen.md +104 -15
  5. data/docs/manual/02-cli-reference.md +43 -12
  6. data/docs/manual/03-configuration.md +20 -7
  7. data/docs/manual/04-diagnostics.md +56 -3
  8. data/docs/manual/05-inspecting-types.md +2 -2
  9. data/docs/manual/06-baseline.md +1 -1
  10. data/docs/manual/07-plugins.md +3 -2
  11. data/docs/manual/08-skills.md +7 -0
  12. data/docs/manual/10-mcp-server.md +40 -4
  13. data/docs/manual/18-removing-dead-code.md +23 -2
  14. data/docs/manual/plugins/rigor-rails-routes.md +2 -2
  15. data/docs/manual/plugins/rigor-rspec.md +101 -1
  16. data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
  17. data/exe/rigor +12 -17
  18. data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
  19. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
  20. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
  21. data/lib/rigor/analysis/check_rules/rule_ids.rb +60 -1
  22. data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
  23. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
  24. data/lib/rigor/analysis/check_rules.rb +274 -51
  25. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  26. data/lib/rigor/analysis/incremental.rb +61 -0
  27. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
  28. data/lib/rigor/analysis/incremental_session.rb +270 -28
  29. data/lib/rigor/analysis/path_expansion.rb +3 -1
  30. data/lib/rigor/analysis/reachability/graph.rb +111 -14
  31. data/lib/rigor/analysis/reachability/scan.rb +305 -23
  32. data/lib/rigor/analysis/rule_catalog.rb +22 -4
  33. data/lib/rigor/analysis/run_cache_key.rb +31 -7
  34. data/lib/rigor/analysis/run_cache_probe.rb +5 -3
  35. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
  36. data/lib/rigor/analysis/runner/pool_coordinator.rb +179 -31
  37. data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
  38. data/lib/rigor/analysis/runner.rb +102 -72
  39. data/lib/rigor/analysis/severity_stamp.rb +45 -1
  40. data/lib/rigor/analysis/worker_session.rb +69 -2
  41. data/lib/rigor/cache/descriptor.rb +79 -12
  42. data/lib/rigor/cache/engine_source.rb +3 -2
  43. data/lib/rigor/cache/incremental_snapshot.rb +39 -5
  44. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  45. data/lib/rigor/cache/store.rb +4 -3
  46. data/lib/rigor/cli/command.rb +3 -1
  47. data/lib/rigor/cli/docs_command.rb +3 -2
  48. data/lib/rigor/cli/doctor_command.rb +1 -1
  49. data/lib/rigor/cli/explain_command.rb +3 -2
  50. data/lib/rigor/cli/mcp_command.rb +1 -1
  51. data/lib/rigor/cli/plugin_command.rb +2 -1
  52. data/lib/rigor/cli/plugins_command.rb +2 -1
  53. data/lib/rigor/cli/sig_gen_command.rb +32 -11
  54. data/lib/rigor/cli/skill_command.rb +2 -1
  55. data/lib/rigor/cli/skill_describe.rb +3 -2
  56. data/lib/rigor/cli/unused_command.rb +10 -7
  57. data/lib/rigor/cli.rb +3 -2
  58. data/lib/rigor/config_audit.rb +163 -22
  59. data/lib/rigor/configuration/dependencies.rb +19 -11
  60. data/lib/rigor/configuration/severity_profile.rb +20 -2
  61. data/lib/rigor/configuration.rb +29 -3
  62. data/lib/rigor/effects/signature_sources.rb +3 -1
  63. data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
  64. data/lib/rigor/environment/gated_signature_guard.rb +2 -1
  65. data/lib/rigor/environment/gated_signature_plan.rb +2 -1
  66. data/lib/rigor/environment/installed_gem_set.rb +2 -1
  67. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
  68. data/lib/rigor/environment/rbs_loader.rb +67 -11
  69. data/lib/rigor/inference/block_call_timing.rb +12 -1
  70. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  71. data/lib/rigor/inference/captured_locals.rb +67 -13
  72. data/lib/rigor/inference/clause_widening.rb +90 -0
  73. data/lib/rigor/inference/definer_resolution.rb +101 -25
  74. data/lib/rigor/inference/expression_typer.rb +94 -9
  75. data/lib/rigor/inference/guard_rebinding.rb +7 -2
  76. data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
  77. data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
  78. data/lib/rigor/inference/macro_block_self_type.rb +20 -6
  79. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
  80. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
  81. data/lib/rigor/inference/method_dispatcher.rb +67 -3
  82. data/lib/rigor/inference/module_function_state.rb +213 -51
  83. data/lib/rigor/inference/narrowing.rb +235 -75
  84. data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
  85. data/lib/rigor/inference/project_patched_methods.rb +168 -4
  86. data/lib/rigor/inference/project_patched_mixins.rb +162 -0
  87. data/lib/rigor/inference/project_patched_scanner.rb +51 -12
  88. data/lib/rigor/inference/project_patched_visibility.rb +214 -0
  89. data/lib/rigor/inference/project_seed_tables.rb +120 -0
  90. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
  91. data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
  92. data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
  93. data/lib/rigor/inference/repeated_or_writes.rb +414 -1
  94. data/lib/rigor/inference/return_barrier.rb +16 -0
  95. data/lib/rigor/inference/scope_indexer.rb +1061 -122
  96. data/lib/rigor/inference/self_call_ivar_writes.rb +346 -0
  97. data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
  98. data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
  99. data/lib/rigor/inference/statement_evaluator.rb +512 -150
  100. data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
  101. data/lib/rigor/lens/anchor.rb +3 -3
  102. data/lib/rigor/lens/contract_reader.rb +60 -2
  103. data/lib/rigor/lens/document.rb +4 -3
  104. data/lib/rigor/lens/file_reader.rb +452 -73
  105. data/lib/rigor/lens/members.rb +81 -17
  106. data/lib/rigor/lens/run.rb +73 -4
  107. data/lib/rigor/lens.rb +2 -1
  108. data/lib/rigor/mcp/server.rb +63 -22
  109. data/lib/rigor/plugin/base.rb +3 -2
  110. data/lib/rigor/plugin/box_probe.rb +154 -7
  111. data/lib/rigor/plugin/io_boundary.rb +42 -6
  112. data/lib/rigor/plugin/isolation.rb +4 -3
  113. data/lib/rigor/project_environment.rb +6 -3
  114. data/lib/rigor/protection/discovery_seed.rb +9 -36
  115. data/lib/rigor/protection/mutation_cache.rb +8 -2
  116. data/lib/rigor/rooted_glob.rb +27 -0
  117. data/lib/rigor/scope/discovery_index.rb +5 -0
  118. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
  119. data/lib/rigor/scope/resolution_chain.rb +40 -5
  120. data/lib/rigor/scope.rb +56 -8
  121. data/lib/rigor/sig_gen/alias_index.rb +2 -1
  122. data/lib/rigor/sig_gen/classification.rb +4 -1
  123. data/lib/rigor/sig_gen/generator.rb +167 -22
  124. data/lib/rigor/sig_gen/layout_index.rb +40 -1
  125. data/lib/rigor/sig_gen/method_candidate.rb +8 -0
  126. data/lib/rigor/sig_gen/observation_collector.rb +137 -11
  127. data/lib/rigor/sig_gen/path_mapper.rb +18 -2
  128. data/lib/rigor/sig_gen/renderer.rb +13 -155
  129. data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
  130. data/lib/rigor/sig_gen/write_reporter.rb +203 -0
  131. data/lib/rigor/sig_gen/write_result.rb +3 -1
  132. data/lib/rigor/sig_gen/writer.rb +50 -35
  133. data/lib/rigor/signature_path_audit.rb +4 -2
  134. data/lib/rigor/type/constant.rb +10 -1
  135. data/lib/rigor/version.rb +1 -1
  136. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  137. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
  138. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
  139. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
  140. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  141. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
  142. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
  143. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
  144. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
  145. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
  146. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
  147. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
  148. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  149. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
  150. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  151. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +57 -6
  152. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
  153. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
  154. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
  155. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
  156. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
  157. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
  158. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
  159. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
  160. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  161. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
  162. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
  163. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
  164. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
  165. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
  166. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
  167. data/sig/rigor/plugin/io_boundary.rbs +5 -2
  168. data/sig/rigor/rooted_glob.rbs +7 -0
  169. data/sig/rigor/scope.rbs +4 -3
  170. data/skills/rigor-doctor/references/01-checks.md +23 -11
  171. data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
  172. metadata +16 -1
@@ -94,7 +94,17 @@ module Rigor
94
94
  does_not_fire_when: [
95
95
  "Implicit-self calls (no receiver) — too noisy without per-method RBS for every helper.",
96
96
  "Receiver is `Dynamic[T]` / `Top` / `Union` — by definition the method set isn't enumerable.",
97
- "Receiver class is in the loader but its RBS definition cannot be built (constant aliases)."
97
+ "Receiver class is in the loader but its RBS definition cannot be built (constant aliases).",
98
+ "Receiver class answers beyond any signature: a plugin-declared open receiver (ADR-26), or one " \
99
+ "Rigor's bundled overlays name (`OpenStruct`, whose fields are per-instance singleton methods; " \
100
+ "`ActiveSupport::Duration` and `ActiveSupport::Concern` when their gem overlay loads). Both sides " \
101
+ "of the class are open, and an inherited Object / Kernel signature no longer decides " \
102
+ "`call.wrong-arity` or `call.argument-type-mismatch` on it.",
103
+ "The program may answer the call another way (#1722): a `method_missing` on the receiver's class or an " \
104
+ "ancestor; a module mixed into either that defines the method (`Integer.include(Ext)` counts wherever " \
105
+ "it is written); singleton methods given to the receiver local (`s.extend(M)`, " \
106
+ "`define_singleton_method`, `class << s`); or, for a receiver typed by its class's name rather than a " \
107
+ "literal, a project subclass that defines the method."
98
108
  ],
99
109
  suppression: "`# rigor:disable call.undefined-method` on the call line, " \
100
110
  "or `disable: [\"call.undefined-method\"]` in `.rigor.yml`.",
@@ -556,14 +566,22 @@ module Rigor
556
566
  summary: "Explicit-receiver call to a method declared `private` in source.",
557
567
  fires_when: [
558
568
  "Call is `receiver.method(...)` with explicit non-self receiver.",
559
- "Receiver type resolves to `Type::Nominal[X]`.",
560
- "X is a user-defined class whose source carries the method under `private`."
569
+ "Receiver type resolves to `Type::Nominal[X]`, or to the class object `singleton(X)`.",
570
+ "X is a user-defined class whose source carries the method under `private`; for a class object, " \
571
+ "the singleton method the call reaches is private (`private_class_method`, `class << self` with " \
572
+ "`private`, or a private method of a module X extends)."
561
573
  ],
562
574
  does_not_fire_when: [
563
575
  "Implicit-self call (no receiver) — always allowed for private.",
564
576
  "Receiver is `self` (Ruby 2.7+ permits `self.private_method`).",
565
577
  "Receiver class is RBS-known but not user-source-defined (RBS-side visibility is deferred).",
566
- "Method is `:protected` (subclass tracking is deferred)."
578
+ "Method is `:protected` (subclass tracking is deferred).",
579
+ "A `method_missing` or `respond_to_missing?` on the receiver's chain, on the side the call reaches, " \
580
+ "may answer the call (Ruby sends a private call to `method_missing`).",
581
+ "The visibility rests on a statement that may not run, or on files that disagree about it.",
582
+ "On a class object: the receiver is not a constant (`self.class.x` may be a subclass that " \
583
+ "re-publicises the name), or the call is not inside a `def` (load-time code may run before the " \
584
+ "method is made private)."
567
585
  ],
568
586
  suppression: "`# rigor:disable method-visibility-mismatch`.",
569
587
  severity_authored: :error,
@@ -70,13 +70,14 @@ module Rigor
70
70
  RUN_EFFECTS_TABLE_PRODUCER_ID = "analysis.run-effects-table"
71
71
 
72
72
  # The run-result producer's declared compaction budget (`Cache::Store#evict!` pass 2). Whole-project,
73
- # but unlike the `rbs.*` producers several generations can be live at once: the `paths` key slot means
74
- # one entry per analyzed-path SET, so `rigor check` over the whole project, over `lib`, and over a
75
- # single file are three separate live generations. 16 is a judgement call sized for that churn; it is
76
- # a cap on GENERATIONS, not on correctness — over-evicting here costs a recompute, never a wrong
77
- # answer. Measured 2026-07-25 against a real project's `.rigor/cache`: the cap does bind (60 distinct
78
- # path-set generations observed, differing in the `paths` slot alone), with no evidence yet on how
79
- # often an evicted generation is asked for again — see issue #151.
73
+ # but unlike the `rbs.*` producers several generations can be live at once: the `paths` and `roots` key
74
+ # slots mean one entry per analyzed-path SET x root list as written (#1559), so `rigor check` over the
75
+ # whole project, over `lib`, and over a single file are three separate live generations, and `rigor
76
+ # check` and `rigor check lib` are two even when `lib` is the configured `paths:`. 16 is a judgement call
77
+ # sized for that churn; it is a cap on GENERATIONS, not on correctness — over-evicting here costs a
78
+ # recompute, never a wrong answer. Measured 2026-07-25 against a real project's `.rigor/cache`: the cap
79
+ # does bind (60 distinct path-set generations observed, differing in the `paths` slot alone), with no
80
+ # evidence yet on how often an evicted generation is asked for again — see issue #151.
80
81
  GENERATION_CAP = 16
81
82
 
82
83
  # The effects sidecar's own compaction budget. One generation per (path set × effects identity), and
@@ -109,6 +110,29 @@ module Rigor
109
110
  nil
110
111
  end
111
112
 
113
+ # #1559 — the run-result key's analysis ROOTS, as the run was given them. The `paths` slot holds only the
114
+ # files the roots expand to, so `check lib extra` with `extra` missing and `check lib` shared a key while
115
+ # only the first prints `extra:1:1: warning: no such file or directory (skipped)`, which changes the exit
116
+ # code under `--fail-on=warning`. The roots are kept in the order and spelling written, not normalised: a
117
+ # missing root is reported as written (`./extra` is not `extra`), and the order of the roots is the order
118
+ # the diagnostics come out in, so two runs that differ only in either are two slots (a forgone hit, never a
119
+ # wrong one).
120
+ #
121
+ # Added to the DIAGNOSTICS key only. The ADR-103 effects sidecar derives its identity from {#descriptor}
122
+ # without it: the effect table is a function of the analysed files, and no path-error row reaches it.
123
+ # The `--incremental` slot builds its own, normalised, roots entry ({IncrementalRunSlot}).
124
+ #
125
+ # @param descriptor — a {#descriptor} result (nil passes through, so a disabled cache stays disabled).
126
+ def with_roots(descriptor, roots)
127
+ return nil if descriptor.nil?
128
+
129
+ entry = config_entry("roots", Marshal.dump(Array(roots).map(&:to_s)))
130
+ Cache::Descriptor.new(
131
+ gems: descriptor.gems, dependencies: descriptor.dependencies, plugins: descriptor.plugins,
132
+ configs: descriptor.configs + [entry]
133
+ )
134
+ end
135
+
112
136
  # Issue #285 — the `engine` slot above pins the engine by VERSION, which identifies the source only
113
137
  # for a released gem. A checkout (a contributor's, or a `bundle add rigor, github:` clone) gets one
114
138
  # extra slot carrying a digest of the engine's own source, so editing `lib/rigor/inference/*.rb` no
@@ -65,9 +65,11 @@ module Rigor
65
65
  # probe must never turn a servable run into a crash.
66
66
  def serve(paths)
67
67
  files = PathExpansion.ruby_files(paths, @configuration.exclude_patterns, base: @configuration.exclude_base)
68
- key = RunCacheKey.descriptor(
69
- configuration: @configuration, files: files, explain: @explain,
70
- rbs_config_entries: RunCacheKey.libraries_config_entries(@configuration)
68
+ key = RunCacheKey.with_roots(
69
+ RunCacheKey.descriptor(
70
+ configuration: @configuration, files: files, explain: @explain,
71
+ rbs_config_entries: RunCacheKey.libraries_config_entries(@configuration)
72
+ ), paths
71
73
  )
72
74
  return nil if key.nil?
73
75
 
@@ -57,7 +57,8 @@ module Rigor
57
57
  quarantined_signatures_snapshot:, env_build_failure_snapshot:,
58
58
  definition_build_failures_snapshot:, hkt_scan_failure_snapshot:,
59
59
  conformance_results_snapshot:, signature_standdowns_snapshot: -> { [] },
60
- vendored_standdowns_snapshot: -> { [] }, pooled_run_disclosures: -> { [] })
60
+ vendored_standdowns_snapshot: -> { [] }, pooled_run_disclosures: -> { [] },
61
+ pooled_trust_refusals: -> { [] })
61
62
  @configuration = configuration
62
63
  @rbs_extended_reporter = rbs_extended_reporter
63
64
  @boundary_cross_reporter = boundary_cross_reporter
@@ -78,6 +79,9 @@ module Rigor
78
79
  # Issue #1051 — reader for the pool coordinator's de-duplicated run-scoped disclosure table.
79
80
  # Empty on a sequential run, where `plugin_registry`'s own instances are the ones that registered.
80
81
  @pooled_run_disclosures_reader = pooled_run_disclosures
82
+ # Issue #1900 — reader for the pool coordinator's summed trust-refusal table. Empty on a
83
+ # sequential run, where `plugin_registry`'s own boundaries made every read.
84
+ @pooled_trust_refusals_reader = pooled_trust_refusals
81
85
  end
82
86
 
83
87
  # Pre-file diagnostic streams that fire once per run rather than per analyzed file: plugin load /
@@ -613,9 +617,18 @@ module Rigor
613
617
  # culprit and reads the same on a cache hit, where every `RBS::Location` has been dumped to a
614
618
  # sentinel. The error class is attributed to that first failure explicitly rather than to all of
615
619
  # them: the per-class memo keeps only the first reason, and one run can mix error classes.
620
+ #
621
+ # Issue #1902 — "first" is first by CLASS NAME, not first recorded. The snapshot arrives in demand
622
+ # order, and that order is a property of the invocation, not of the project: an `--incremental`
623
+ # recheck records the classes its closure demanded before it unions in the replayed set, and a worker
624
+ # pool unions per-worker lists in drain order. So the classes are sorted by name and the files
625
+ # sorted and deduplicated here. Which tuple each class carries is fixed upstream
626
+ # ({Environment::RbsLoader.definition_build_detail_order}), not here.
616
627
  def build_rbs_definition_build_failed_diagnostic(failures)
628
+ failures = failures.sort_by { |failure| failure.first.to_s }
617
629
  sample_size = 5
618
- files = failures.flat_map { |failure| Array(failure[3]) }.uniq.map { |name| relative_signature_path(name) }
630
+ files = failures.flat_map { |failure| Array(failure[3]) }
631
+ .map { |name| relative_signature_path(name) }.uniq.sort
619
632
  Diagnostic.new(
620
633
  path: ".rigor.yml",
621
634
  line: 1,
@@ -1084,16 +1097,22 @@ module Rigor
1084
1097
  # cache"). It is also never routed through `IncrementalSession`'s per-file cache (`#788`): that cache
1085
1098
  # holds only `Runner#per_file_diagnostics`, and this row is not part of that stream.
1086
1099
  #
1087
- # Scope: `plugin_registry` here is the coordinator-side registry, which only actually RUNS `#prepare`
1088
- # / per-file analysis in sequential mode (`--workers` unset, the default) — a pooled run (`--workers
1089
- # N`) prepares and analyses on per-worker `WorkerSession` registries this reader never sees, so a
1090
- # refusal confined to a pooled worker's slice is not reported. Left out deliberately: draining it
1091
- # needs a fourth marshalled channel beside `drain_reporters` / `drain_dependencies` / `drain_effects`
1092
- # in `Runner::PoolCoordinator`, and the default (unpooled) path already covers the reported bug.
1100
+ # Two sources (issue #1900), summed per plugin: the coordinator-side registry's own boundaries — where
1101
+ # a sequential run, and the pool's no-worker fallback, made every read — and `pooled_trust_refusals`,
1102
+ # what a pooled run drained from its pre-fork parent session and every worker
1103
+ # ({Runner::PoolCoordinator#merge_trust_refusals}). The count is per refused ATTEMPT, as
1104
+ # `--workers 0` always counted it; the pool drains the parent's `#prepare` refusals before forking and
1105
+ # each worker reports only what it added, so the workers, which inherit the parent's boundaries, do
1106
+ # not count them again. Rows follow registry
1107
+ # (plugin load) order, which every mode shares.
1093
1108
  def plugin_trust_refusal_diagnostics
1094
1109
  return [] if plugin_registry.empty?
1095
1110
 
1096
- plugin_registry.plugins.filter_map { |plugin| plugin_trust_refusal_diagnostic(plugin) }
1111
+ pooled = Array(pooled_trust_refusals).to_h { |record| [record[:plugin_id], record] }
1112
+ plugin_registry.plugins.filter_map do |plugin|
1113
+ summary = combined_refusal_summary(pooled[disclosure_plugin_id(plugin)], plugin)
1114
+ plugin_trust_refusal_diagnostic(plugin, summary) if summary
1115
+ end
1097
1116
  end
1098
1117
 
1099
1118
  # Issue #1051 — the run-scoped plugin disclosures ({Plugin::Base#disclose_once}), the run's
@@ -1193,10 +1212,23 @@ module Rigor
1193
1212
  plugin.class.to_s
1194
1213
  end
1195
1214
 
1196
- def plugin_trust_refusal_diagnostic(plugin)
1197
- summary = plugin.io_boundary.refusal_summary
1198
- return nil if summary.nil?
1215
+ # The coordinator's own record first when it has one: on a pooled run it holds the `#init` reads
1216
+ # (every registry runs `#init`; the workers' drains start after it), which `--workers 0` makes before
1217
+ # `#prepare` and before any file, so its first path is the one a sequential run names. Not so after a
1218
+ # Ractor-backend degrade, where this record also holds the re-analysed files' refusals and is named
1219
+ # ahead of a worker's `#prepare` path; the count is right either way.
1220
+ def combined_refusal_summary(pooled, plugin)
1221
+ local = begin
1222
+ plugin.io_boundary.refusal_summary
1223
+ rescue StandardError
1224
+ nil
1225
+ end
1226
+ return local || pooled if pooled.nil? || local.nil?
1227
+
1228
+ local.merge(count: local[:count] + pooled[:count])
1229
+ end
1199
1230
 
1231
+ def plugin_trust_refusal_diagnostic(plugin, summary)
1200
1232
  plugin_id = plugin.manifest.id
1201
1233
  Diagnostic.new(
1202
1234
  path: ".rigor.yml", line: 1, column: 1,
@@ -1283,6 +1315,10 @@ module Rigor
1283
1315
  @pooled_run_disclosures_reader.call
1284
1316
  end
1285
1317
 
1318
+ def pooled_trust_refusals
1319
+ @pooled_trust_refusals_reader.call
1320
+ end
1321
+
1286
1322
  def pre_eval_diagnostics_from_scanner
1287
1323
  @pre_eval_diagnostics_from_scanner_reader.call
1288
1324
  end