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
@@ -26,6 +26,7 @@ require_relative "../effects/identity"
26
26
  require_relative "../effects/propagator"
27
27
  require_relative "../inference/parameter_inference_collector"
28
28
  require_relative "../inference/pre_eval_constants"
29
+ require_relative "../inference/project_seed_tables"
29
30
  require_relative "../inference/scope_indexer"
30
31
  require_relative "../inference/synthetic_method_scanner"
31
32
  require_relative "../inference/project_patched_scanner"
@@ -98,6 +99,16 @@ module Rigor
98
99
  pooled.empty? ? @file_dependencies : @file_dependencies.merge(pooled)
99
100
  end
100
101
 
102
+ # Issue #1553 — the plugin reads ({Plugin::IoBoundary.attributing}) each file's analysis made this run,
103
+ # `{path => [row]}`, reconciled from the sequential and pooled halves exactly as {#file_dependencies} is.
104
+ # Empty unless `record_dependencies: true`; a file whose analysis read nothing has no key. The incremental
105
+ # session keeps them beside each file's cached rows so a later recheck re-analyses the file once one of
106
+ # them goes stale.
107
+ def analysis_reads
108
+ pooled = @pool_coordinator.collected_analysis_reads
109
+ pooled.empty? ? @analysis_reads : @analysis_reads.merge(pooled)
110
+ end
111
+
101
112
  # ADR-103 — the run's propagated effect graph, or {Effects::EffectTable.empty} when collection did
102
113
  # not run. Reconciled from the sequential and pooled halves exactly as {#file_dependencies} is, then
103
114
  # closed by the post-pool fixpoint in {#assemble_run_diagnostics}.
@@ -297,7 +308,8 @@ module Rigor
297
308
  record_dependencies: false, record_self_calls: false, analyze_only: nil,
298
309
  seed_bundles: nil, collect_seed_bundles: false, param_inferred_types: nil,
299
310
  discovery_seed: nil, no_tolerated_effects: false,
300
- restored_run_level_rows: nil, served_per_file_diagnostics: nil, on_file_indexed: nil)
311
+ restored_run_level_rows: nil, served_per_file_diagnostics: nil, served_effect_collections: nil,
312
+ on_file_indexed: nil)
301
313
  @configuration = configuration
302
314
  # ADR-113 WD2 — `rigor lens` reads a target's seeded index here: called with `(path, parse_result, scope,
303
315
  # index)` for every file this process indexes, after its rules ran and inside the run window, so the reader
@@ -311,6 +323,10 @@ module Rigor
311
323
  # narrowed run does not analyse, spliced into this run's per-file stream by
312
324
  # {#interleave_served_diagnostics}. Empty on every other run.
313
325
  @served_per_file_diagnostics = served_per_file_diagnostics || {}
326
+ # Issue #1898 — `{path => FileCollection}` the {IncrementalSession} serves for the same unanalysed files, so the
327
+ # effect graph this run closes (and the envelope pass judges) spans the whole project, not just the closure.
328
+ # Never part of {#effect_collections_by_path}, which stays what THIS run collected. Empty on every other run.
329
+ @served_effect_collections = served_effect_collections || {}
314
330
  @explain = explain
315
331
  @cache_store = enforce_read_only_cache(cache_store, buffer)
316
332
  @plugin_requirer = plugin_requirer
@@ -459,6 +475,9 @@ module Rigor
459
475
  @project_struct_member_layouts = {}.freeze
460
476
  # Issue #684 — set per run by {#project_discovery_expansion}; nil on every path that does not widen.
461
477
  @project_discovery_expansion = nil
478
+ # Issue #1583 — whether this run widened its discovery to the configured paths, even when the widening
479
+ # added no file; {#discovery_glob_entries} lists the configured roots for every such run.
480
+ @discovery_widened = false
462
481
  # ADR-67 WD6a — the call-site parameter-inference table, populated by the opt-in pre-pass in
463
482
  # `assemble_run_diagnostics` when `parameter_inference:` is enabled, then seeded onto every per-file
464
483
  # scope (sequential + fork-worker) through `project_scope_seed_tables`. Empty by default, so the gate-off
@@ -513,6 +532,7 @@ module Rigor
513
532
 
514
533
  expansion = expand_paths(paths)
515
534
  @project_discovery_expansion = project_discovery_expansion(paths, expansion)
535
+ @discovery_widened = widen_discovery_to_project?(paths)
516
536
  @snapshots.reset_for_run
517
537
  # Per-run reset of the deferred-discovery memo (see `#ensure_project_discovery`).
518
538
  @project_discovery_done = false
@@ -594,6 +614,19 @@ module Rigor
594
614
  expand_paths(paths).fetch(:files)
595
615
  end
596
616
 
617
+ # Issue #1583 — the project files a run over `paths` would DISCOVER without analysing them (#684's
618
+ # widening), by globbing only. Empty whenever the run would not widen, which includes every run over
619
+ # the configured `paths:`. {IncrementalSession} digests them, because an edit to one moves the
620
+ # diagnostics of the files it does analyse.
621
+ def discovery_only_file_set(paths = @configuration.paths)
622
+ expansion = expand_paths(paths)
623
+ widened = project_discovery_expansion(paths, expansion)
624
+ return [] if widened.nil?
625
+
626
+ analysed = expansion.fetch(:files).to_set
627
+ widened.fetch(:files).reject { |path| analysed.include?(path) }
628
+ end
629
+
597
630
  # ADR-46 §2 — inverts {#file_dependencies} into the reverse edge the incremental step walks:
598
631
  # `dependents[X] = { A : A read a declaration / body from X }`. On an edit to X, the body tier
599
632
  # (slice 2) re-analyses `{X} ∪ dependents[X]` and serves every other file from the per-file cache.
@@ -786,6 +819,7 @@ module Rigor
786
819
  @project_discovery_done = false
787
820
  # This entry point supplies its own expansion; it never widens (see {#project_discovery_expansion}).
788
821
  @project_discovery_expansion = nil
822
+ @discovery_widened = false
789
823
  @run_generation = Object.new.freeze
790
824
  run_project_pre_passes(expansion: expansion)
791
825
  ensure_project_discovery(expansion)
@@ -851,7 +885,8 @@ module Rigor
851
885
  analysis = serve_effect_collections(expansion, environment, key_descriptor, rbs_descriptor)
852
886
  computed = false
853
887
  diagnostics = @cache_store.fetch_or_validate(
854
- producer_id: RunCacheKey::RUN_DIAGNOSTICS_PRODUCER_ID, key_descriptor: key_descriptor,
888
+ producer_id: RunCacheKey::RUN_DIAGNOSTICS_PRODUCER_ID,
889
+ key_descriptor: RunCacheKey.with_roots(key_descriptor, expansion.fetch(:roots)),
855
890
  generation_cap: RunCacheKey::GENERATION_CAP
856
891
  ) do
857
892
  computed = true
@@ -1380,17 +1415,32 @@ module Rigor
1380
1415
 
1381
1416
  def build_run_dependency_descriptor(expansion, rbs_descriptor)
1382
1417
  entries = analyzed_file_entries(expansion) + discovery_file_entries(expansion) +
1383
- pre_eval_file_entries + template_unit_file_entries + rbs_descriptor.files
1418
+ pre_eval_file_entries + template_unit_file_entries + rbs_descriptor.files +
1419
+ root_presence_entries(expansion)
1384
1420
  # #979 — the signature ROOTS' listings, one glob row each. `rbs_descriptor.files` covers only the
1385
1421
  # `.rbs` files that existed while the run read them, so without these a signature file written after
1386
1422
  # the run left the slot validating fresh and the warm run answered without it.
1387
- globs = rbs_descriptor.globs + template_unit_glob_entries
1423
+ globs = rbs_descriptor.globs + template_unit_glob_entries + discovery_glob_entries
1388
1424
  boundary = plugin_boundary_rows
1389
1425
  entries.concat(boundary.files)
1390
1426
  globs.concat(boundary.globs).concat(producer_watch_glob_entries)
1391
1427
  Cache::Descriptor.new(files: entries, globs: globs.uniq)
1392
1428
  end
1393
1429
 
1430
+ # #1962 / #1555 — a kind row per path the run's answer depends on the presence and kind of, not only its
1431
+ # bytes. An analysis root: its path errors (`no such file or directory`, `not a Ruby file`) answer to whether
1432
+ # it exists and whether it is a directory or a file, while what a directory root holds is the analysed-file
1433
+ # set the key carries. A `pre_eval:` entry: `pre-eval.file-not-found` answers to whether it is a file (one
1434
+ # that is has its `:stat` row from {#pre_eval_file_entries}). A project signature root, configured or the
1435
+ # auto-detected `sig`: the loader reads a directory and a file differently, and #979's listing rows cover a
1436
+ # file appearing under a root that existed, not the root appearing. A kind row rather than an existence row,
1437
+ # because a directory replaced by a file of the same name leaves an existence row fresh. The `--incremental`
1438
+ # slot records kind rows for the same paths ({#incremental_slot_rows}, {#baseline_dependency_rows}, #1977).
1439
+ def root_presence_entries(expansion)
1440
+ (expansion.fetch(:roots) + @configuration.pre_eval + signature_root_candidates)
1441
+ .uniq.map { |path| Cache::Descriptor::FileEntry.kind(path: path) }
1442
+ end
1443
+
1394
1444
  # Every file a plugin read or probed through its `IoBoundary` (#577 / #613), and every directory it listed
1395
1445
  # (#954), as the boundaries stand when asked.
1396
1446
  def plugin_boundary_rows
@@ -1431,11 +1481,11 @@ module Rigor
1431
1481
  # globs, which a producer replays into its boundary whether it computed or was served (#1558). A save
1432
1482
  # after the read leaves the row stale, so no guard is needed.
1433
1483
  # - `derived` — rows taken when the run ENDS: the template files and their claimed globs (#392, re-analysed
1434
- # every run), and one existence row per analysis root and per `pre_eval:` entry. A run's path errors (`no
1435
- # such file or directory`, `pre-eval.file-not-found`) answer to those last rows; the plain slot records
1436
- # neither, and the incremental path regenerates the errors every run, so a slot that served one past the
1437
- # edit that retracts it would print what no run of that tree prints. Existence rows carry ADR-45 WD1b's
1438
- # bound: a directory replaced by a file of the same name stays fresh.
1484
+ # every run), and one `:kind` row per analysis root and per `pre_eval:` entry. A run's path errors (`no
1485
+ # such file or directory`, `not a Ruby file`, `pre-eval.file-not-found`) answer to those last rows, and the
1486
+ # incremental path regenerates the errors every run, so a slot that served one past the edit that retracts
1487
+ # it would print what no run of that tree prints. A kind row rather than an existence row (#1977): an
1488
+ # existence row reads a directory replaced by a file of the same name as fresh, ADR-45 WD1b's bound.
1439
1489
  # - `by_file` — `{path => Descriptor}`, the plugin reads each file's analysis made
1440
1490
  # ({Plugin::IoBoundary.attributing}). A narrowed run analyses only its closure, so the session carries the
1441
1491
  # other files' reads forward from the previous slot.
@@ -1459,7 +1509,7 @@ module Rigor
1459
1509
  IncrementalSlotRows.new(
1460
1510
  observed: plugin_boundary_rows,
1461
1511
  derived: Cache::Descriptor.new(
1462
- files: template_unit_file_entries + existence_entries(Array(roots) + @configuration.pre_eval),
1512
+ files: template_unit_file_entries + kind_entries(Array(roots) + @configuration.pre_eval),
1463
1513
  globs: template_unit_glob_entries
1464
1514
  ),
1465
1515
  by_file: @analysis_reads.transform_values do |reads|
@@ -1486,13 +1536,14 @@ module Rigor
1486
1536
  # which the session therefore carries unchanged from the full run that recorded them to every later slot:
1487
1537
  #
1488
1538
  # - the signature tree the plain slot records (`RbsDescriptor.file_entries` / `.glob_entries` over the loader
1489
- # roots this run's analysis resolved), and an existence row per signature root, configured or the
1490
- # auto-detected `sig` — a recheck rebuilds no environment unless its closure is non-empty, and the snapshot
1491
- # fingerprint digests the same roots (configured, or the auto-detected `sig`), so a change to one drops the
1492
- # snapshot and forces a full run rather than reaching a recheck;
1539
+ # roots this run's analysis resolved), and a `:kind` row per signature root, configured or the
1540
+ # auto-detected `sig` — a recheck rebuilds no environment unless its closure is non-empty. The snapshot
1541
+ # fingerprint digests the same roots, so a change to one makes the next full-path run a full run rather
1542
+ # than a recheck, but the engine-free slot probe serves before any fingerprint is consulted: a root that
1543
+ # turns from a file into a directory (or back) is declined there by this `:kind` row alone (#1977);
1493
1544
  # - the discovered-not-analysed files (#684) and a listing row per discovery root — a narrowed recheck does
1494
1545
  # not widen discovery at all;
1495
- # - every `pre_eval:` file outside `files` (a stat row, or an absence row) — the session tracks only analysed
1546
+ # - every `pre_eval:` file outside `files` (a stat row, or a kind row) — the session tracks only analysed
1496
1547
  # files, and one that is analysed is guarded by its own row.
1497
1548
  #
1498
1549
  # Recomputing any of these on a recheck would vouch for the tree the recheck saw while its answer still
@@ -1516,7 +1567,7 @@ module Rigor
1516
1567
  own, pinned = signature_rows
1517
1568
  BaselineRows.new(
1518
1569
  owned: Cache::Descriptor.new(
1519
- files: own.files + existence_entries(signature_root_candidates) +
1570
+ files: own.files + kind_entries(signature_root_candidates) +
1520
1571
  discovery_file_entries({ files: files }) + unanalysed_pre_eval_entries(files),
1521
1572
  globs: (own.globs + discovery_glob_entries).uniq
1522
1573
  ),
@@ -1542,16 +1593,18 @@ module Rigor
1542
1593
 
1543
1594
  # The project's own signature roots, whether or not they exist: the configured `signature_paths:`, or the
1544
1595
  # `sig` directory `Environment.default_signature_paths` auto-detects when none are configured. The
1545
- # signature rows' listing rows cover a file appearing under a root that existed; only an existence row
1546
- # covers the root itself appearing.
1596
+ # signature rows' listing rows cover a file appearing under a root that existed; only a row for the root
1597
+ # itself covers it appearing.
1547
1598
  def signature_root_candidates
1548
1599
  @configuration.signature_paths || ["sig"]
1549
1600
  end
1550
1601
 
1551
1602
  # One names-mode listing row per directory the discovery widening globbed, so a file appearing there is seen
1552
- # as well as an edit to one that existed.
1603
+ # as well as an edit to one that existed. Issue #1583 — keyed on the widening decision, not on what it
1604
+ # added: a run over `lib/b.rb` alone in `lib` widens to nothing, and a `lib/a.rb` created afterwards must
1605
+ # still move the answer of the run-result cache and the `--incremental` slot.
1553
1606
  def discovery_glob_entries
1554
- return [] if @project_discovery_expansion.nil?
1607
+ return [] unless @discovery_widened
1555
1608
 
1556
1609
  @configuration.paths.select { |root| File.directory?(root) }.map do |root|
1557
1610
  Cache::Descriptor::GlobEntry.compute(root: root, pattern: PathExpansion::RUBY_GLOB, mode: :names)
@@ -1565,22 +1618,18 @@ module Rigor
1565
1618
  if File.file?(path)
1566
1619
  Cache::Descriptor::FileEntry.stat(path: path, digest: Cache::FileDigest.hexdigest(path))
1567
1620
  else
1568
- existence_entries([path]).first
1621
+ kind_entries([path]).first
1569
1622
  end
1570
1623
  end
1571
1624
  end
1572
1625
 
1573
- def existence_entries(paths)
1626
+ # #1977 — one `:kind` row per path, the row {#root_presence_entries} records in the plain slot.
1627
+ def kind_entries(paths)
1574
1628
  paths.uniq.map do |path|
1575
- physical = @buffer ? @buffer.resolve(path) : path
1576
- if File.exist?(physical)
1577
- Cache::Descriptor::FileEntry.present(path: physical)
1578
- else
1579
- Cache::Descriptor::FileEntry.absent(path: physical)
1580
- end
1629
+ Cache::Descriptor::FileEntry.kind(path: @buffer ? @buffer.resolve(path) : path)
1581
1630
  end
1582
1631
  end
1583
- private :plugin_boundary_rows, :producer_watch_glob_entries, :existence_entries,
1632
+ private :plugin_boundary_rows, :producer_watch_glob_entries, :kind_entries, :root_presence_entries,
1584
1633
  :analysis_reads_attributed?, :signature_root_candidates, :discovery_glob_entries,
1585
1634
  :unanalysed_pre_eval_entries, :signature_rows, :within?
1586
1635
 
@@ -1804,8 +1853,14 @@ module Rigor
1804
1853
  # on what else was on the command line.
1805
1854
  #
1806
1855
  # Excluded, each because its expansion is already the one it means: the LSP `prebuilt:` path (which
1807
- # seeds discovery from a snapshot), editor `buffer:` mode, `run_source`'s in-memory single file, and
1808
- # ADR-46's `analyze_only` subset mode (which already passes the full expansion and filters later).
1856
+ # seeds discovery from a snapshot), editor `buffer:` mode, and `run_source`'s in-memory single file.
1857
+ #
1858
+ # Issue #1583 — ADR-46's `analyze_only` subset mode widens like a full run. It was excluded on the
1859
+ # reading that it "already passes the full expansion", which holds only when the incremental session
1860
+ # runs over the configured `paths:`. `rigor check --incremental lib/b.rb` passes the argument's
1861
+ # expansion, so its rechecks (and `--verify-incremental`'s partitions) discovered the argument alone
1862
+ # while its baseline discovered the project, and `lib/b.rb`'s report on a sibling's constant came and
1863
+ # went with the run kind.
1809
1864
  #
1810
1865
  # Issue #1556 — the widened set holds each FILE once. `.rigor.yml`'s `paths:` load as absolute paths and a
1811
1866
  # path argument usually does not, so `rigor check lib` widened discovery over both spellings of every file:
@@ -1843,8 +1898,7 @@ module Rigor
1843
1898
  end
1844
1899
 
1845
1900
  def widen_discovery_to_project?(paths)
1846
- @prebuilt.nil? && @buffer.nil? && @in_memory_sources.nil? && @analyze_only.nil? &&
1847
- paths != @configuration.paths
1901
+ @prebuilt.nil? && @buffer.nil? && @in_memory_sources.nil? && paths != @configuration.paths
1848
1902
  end
1849
1903
 
1850
1904
  # ADR-46 — the dependency-recording and subset-analysis modes read the discovery tables OUTSIDE the
@@ -1907,8 +1961,10 @@ module Rigor
1907
1961
  # The run's per-file effect collections in sorted path order — the sequential half merged with the
1908
1962
  # pool's, exactly as {#file_dependencies} reconciles its two halves. Sorted, because the fold below
1909
1963
  # it must not depend on pool-completion order.
1964
+ # Issue #1898 — a narrowed incremental run adds the collections its session served for the files it did not
1965
+ # analyse, under this run's own for any path both name.
1910
1966
  def effect_collections
1911
- effect_collections_by_path.sort_by { |path, _| path.to_s }.map(&:last)
1967
+ @served_effect_collections.merge(effect_collections_by_path).sort_by { |path, _| path.to_s }.map(&:last)
1912
1968
  end
1913
1969
 
1914
1970
  # Editor mode § "Scope choice — option A". Under `buffer:` non-nil the per-file analysis emits
@@ -2024,7 +2080,8 @@ module Rigor
2024
2080
  analyze_file: ->(path, environment) { analyze_file(path, environment) },
2025
2081
  restored_run_level_rows: @restored_run_level_rows,
2026
2082
  feature_sources: -> { required_feature_sources },
2027
- plugin_prepare: -> { @pre_passes.prepare_throwaway_registry }
2083
+ plugin_prepare: -> { @pre_passes.prepare_throwaway_registry },
2084
+ prepare_coordinator_plugins: -> { @pre_passes.prepare_registry(@plugin_registry) }
2028
2085
  )
2029
2086
  @diagnostic_aggregator = DiagnosticAggregator.new(
2030
2087
  configuration: @configuration,
@@ -2044,7 +2101,8 @@ module Rigor
2044
2101
  definition_build_failures_snapshot: -> { @snapshots.definition_build_failures },
2045
2102
  hkt_scan_failure_snapshot: -> { @snapshots.hkt_scan_failure },
2046
2103
  conformance_results_snapshot: -> { @snapshots.conformance_results },
2047
- pooled_run_disclosures: -> { @pool_coordinator.collected_run_disclosures }
2104
+ pooled_run_disclosures: -> { @pool_coordinator.collected_run_disclosures },
2105
+ pooled_trust_refusals: -> { @pool_coordinator.collected_trust_refusals }
2048
2106
  )
2049
2107
  end
2050
2108
 
@@ -2197,7 +2255,7 @@ module Rigor
2197
2255
  bad << [path, "no such file or directory"]
2198
2256
  end
2199
2257
  end
2200
- { files: files, errors: path_expansion_errors(bad, any_files: files.any?) }
2258
+ { files: files, roots: Array(paths).map(&:to_s), errors: path_expansion_errors(bad, any_files: files.any?) }
2201
2259
  end
2202
2260
 
2203
2261
  # A bad path *among valid ones* is a warn-and-skip — the run still does useful work, so `rigor check
@@ -2400,50 +2458,21 @@ module Rigor
2400
2458
  tables[:published_constant_names] = names unless names.empty?
2401
2459
  end
2402
2460
 
2403
- # Issue #617 — the censused names that bind, grouped by last segment (`Scope#bound_constant_names`). A
2404
- # name some write other than a memo `||=` touches always binds. A memo-only name binds once memos of its
2405
- # segment span two files: either may load first and set the constant the other then reads, so only a
2406
- # memo no other file shares keeps the idiom's reading. Files are counted expanded, because discovery can
2407
- # walk one file under two spellings (`lib/b.rb`, `./lib/b.rb`).
2461
+ # Issue #617 / #1290 — the binding and shadowing constant names, by last segment; derived by
2462
+ # {Inference::ProjectSeedTables}, which the seed paths that run no `check` share.
2408
2463
  def constant_writer_index
2409
- @constant_writer_index ||= begin
2410
- memo_files = memo_files_by_segment
2411
- @project_constant_writes.each_with_object({}) do |(name, by_path), index|
2412
- segment = name.split("::").last
2413
- next if memo_only?(by_path) && memo_files.fetch(segment).size < 2
2414
-
2415
- (index[segment] ||= []) << name
2416
- end.each_value(&:freeze).freeze
2417
- end
2464
+ @constant_writer_index ||= Inference::ProjectSeedTables.constant_writers(@project_constant_writes)
2418
2465
  end
2419
2466
 
2420
- # Issue #1290 — the censused names some write other than a memo `||=` assigns, grouped by last segment
2421
- # (`Scope#shadowing_constant_names`): the ones that exist whether or not an outer constant of the name
2422
- # does, and so stop the lexical ladder above it. A memo-only name is left out however many files memoize
2423
- # it, unlike in {#constant_writer_index}: `X ||= v` resolves `X` through the very lookup whose lower rung
2424
- # the stop would replace, so wherever that rung answers, the memo finds it and never creates the name.
2425
2467
  def constant_shadower_index
2426
- @constant_shadower_index ||= @project_constant_writes.each_with_object({}) do |(name, by_path), index|
2427
- (index[name.split("::").last] ||= []) << name unless memo_only?(by_path)
2428
- end.each_value(&:freeze).freeze
2468
+ @constant_shadower_index ||= Inference::ProjectSeedTables.constant_shadowers(@project_constant_writes)
2429
2469
  end
2430
2470
 
2431
- def memo_files_by_segment
2432
- @project_constant_writes.each_with_object({}) do |(name, by_path), files|
2433
- set = (files[name.split("::").last] ||= Set.new)
2434
- by_path.each { |path, descriptor| set << File.absolute_path(path) if memo_descriptor?(descriptor) }
2435
- end
2436
- end
2437
-
2438
- def memo_only?(by_path) = by_path.each_value.all? { |descriptor| memo_descriptor?(descriptor) }
2439
- def memo_descriptor?(descriptor) = descriptor == Inference::ScopeIndexer::CONSTANT_MEMO
2440
-
2441
2471
  # Issue #644 — the LAST SEGMENTS of the published table, the run-wide half of
2442
2472
  # `Scope#published_constant?`. Seeded on EVERY run (unlike `constant_sources`, which only a recording
2443
2473
  # run needs) because {CheckRules::PublishedConstantGuard} asks the question on every `if` / `unless`.
2444
2474
  def published_constant_name_set
2445
- @published_constant_name_set ||=
2446
- @project_constant_values.keys.to_set { |name| name.split("::").last }.freeze
2475
+ @published_constant_name_set ||= Inference::ProjectSeedTables.published_constant_names(@project_constant_values)
2447
2476
  end
2448
2477
 
2449
2478
  # Issue #644 — the seeded `in_source_constants` table, joining the two cross-file constant producers.
@@ -2654,7 +2683,8 @@ module Rigor
2654
2683
  node_collectors: node_collectors,
2655
2684
  stated_runtime_ruby: @configuration.stated_runtime_ruby
2656
2685
  )
2657
- diagnostics + plugin_emitted_diagnostics(path, root, scope, node_results)
2686
+ plugin_rows = plugin_emitted_diagnostics(path, root, scope, node_results)
2687
+ diagnostics + CheckRules.filter_plugin_suppressed(plugin_rows, path: path, comments: parse_result.comments)
2658
2688
  end
2659
2689
 
2660
2690
  # ADR-24 slice 4a — runs `block` (the typing pass) with the self-call recorder active when either
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "check_rules/rule_ids"
3
4
  require_relative "diagnostic"
4
5
  require_relative "../configuration/severity_profile"
5
6
 
@@ -9,14 +10,55 @@ module Rigor
9
10
  # overrides and drops any diagnostic resolving to `:off`. Rules emit with their authored severity; the
10
11
  # profile is the final filter.
11
12
  #
13
+ # Issue #1895 — the `.rigor.yml` `disable:` list is applied here too, ahead of the profile, with the same
14
+ # token expansion and match per-file analysis uses ({CheckRules.config_disabled?}), but only to a built-in
15
+ # check-rule row (see {.check_rule_row?}): the rows the diagnostic policy already says `disable:` reaches. Most
16
+ # are filtered where they were produced; this catches the ones that are not (the `effect.annotations-unchecked`
17
+ # residual under `disable: [all]`) on every run shape (sequential, pool, `--incremental`, a warm cache hit).
18
+ # Issue #1953 — a plugin's `plugin.<id>.*` row answers here to its qualified id (`plugin.<id>.<rule>`) or to
19
+ # `plugin.<id>`, never to a bare id or `all`. The run-level rows outside the check-rule catalogue
20
+ # (`rbs.coverage.*`, `dynamic.dependency-source.*`, the plugin loader, `plugin_trust.*`, `pool-degraded`) keep
21
+ # answering to `severity_overrides:` only, by any spelling.
22
+ # Issue #1985 — `severity_overrides:` follows the same rule: a plugin's row answers to its qualified id or
23
+ # `plugin.<id>`, never to its bare rule or a family wildcard; the plugin loader's rows answer to
24
+ # `plugin_loader.<rule>` first and to their bare `load-error` / `runtime-error` after it.
25
+ #
12
26
  # Extracted (ADR-87 WD4) so the miss path ({Runner::DiagnosticAggregator}) and the boot-slimming hit path
13
27
  # ({RunCacheProbe}, which never loads the aggregator) apply the IDENTICAL final filter — a warm HIT's
14
28
  # served diagnostics are byte-identical to what a full run would print.
15
29
  module SeverityStamp
16
30
  module_function
17
31
 
32
+ CHECK_RULES = CheckRules::ALL_RULES.to_set.freeze
33
+ private_constant :CHECK_RULES
34
+
18
35
  def apply(diagnostics, configuration)
19
- diagnostics.filter_map { |diagnostic| stamp(diagnostic, configuration) }
36
+ disabled = CheckRules.expand_rule_tokens(configuration.disabled_rules)
37
+ diagnostics.filter_map do |diagnostic|
38
+ next nil if config_disabled_row?(diagnostic, disabled)
39
+
40
+ stamp(diagnostic, configuration)
41
+ end
42
+ end
43
+
44
+ # A built-in check-rule row answers to the expanded list by its id or `all`; a plugin's `plugin.<id>.*` row
45
+ # by its qualified id or `plugin.<id>` only ({CheckRules.plugin_row_disabled?}, issue #1953). Every other
46
+ # row is out of the list's reach.
47
+ def config_disabled_row?(diagnostic, disabled)
48
+ return CheckRules.config_disabled?(diagnostic.rule, disabled) if check_rule_row?(diagnostic)
49
+
50
+ CheckRules.plugin_row_disabled?(diagnostic, disabled)
51
+ end
52
+
53
+ # A built-in check-rule row: its id is in the CheckRules catalogue and no plugin emitted it.
54
+ def check_rule_row?(diagnostic)
55
+ diagnostic.source_family == Diagnostic::DEFAULT_SOURCE_FAMILY && CHECK_RULES.include?(diagnostic.rule)
56
+ end
57
+
58
+ # `plugin.<id>` for a row a plugin emitted, nil for every other row.
59
+ def plugin_family(diagnostic)
60
+ family = diagnostic.source_family.to_s
61
+ family if family.start_with?(CheckRules::PLUGIN_FAMILY_PREFIX)
20
62
  end
21
63
 
22
64
  def stamp(diagnostic, configuration)
@@ -24,6 +66,8 @@ module Rigor
24
66
 
25
67
  resolved = Configuration::SeverityProfile.resolve(
26
68
  rule: diagnostic.rule,
69
+ qualified_rule: diagnostic.qualified_rule,
70
+ plugin_family: plugin_family(diagnostic),
27
71
  authored_severity: diagnostic.severity,
28
72
  profile: configuration.severity_profile,
29
73
  overrides: configuration.severity_overrides,
@@ -170,8 +170,13 @@ module Rigor
170
170
  return analyze_with_effects(path) unless @record_dependencies
171
171
 
172
172
  diagnostics = nil
173
- record = DependencyRecorder.record_for(path) { diagnostics = analyze_with_effects(path) }
173
+ reads = []
174
+ record = Plugin::IoBoundary.attributing(reads) do
175
+ DependencyRecorder.record_for(path) { diagnostics = analyze_with_effects(path) }
176
+ end
174
177
  @file_dependencies[path] = record
178
+ # Issue #1553 — the plugin reads this file's analysis made, as `Runner#analyze_file` keeps them.
179
+ (@analysis_reads ||= {})[path] = reads unless reads.empty?
175
180
  diagnostics
176
181
  end
177
182
 
@@ -241,7 +246,8 @@ module Rigor
241
246
  node_collectors: node_collectors,
242
247
  stated_runtime_ruby: @configuration.stated_runtime_ruby
243
248
  )
244
- diagnostics += plugin_emitted_diagnostics(path, root, scope, node_results)
249
+ plugin_rows = plugin_emitted_diagnostics(path, root, scope, node_results)
250
+ diagnostics += CheckRules.filter_plugin_suppressed(plugin_rows, path: path, comments: parse_result.comments)
245
251
  diagnostics + explain_diagnostics(path, parse_result.value, scope)
246
252
  rescue Errno::ENOENT => e
247
253
  [analyzer_error(path, e.message)]
@@ -308,6 +314,12 @@ module Rigor
308
314
  @file_dependencies
309
315
  end
310
316
 
317
+ # Issue #1553 — the plugin reads ({Plugin::IoBoundary.attributing}) each `#analyze` call's file made (empty
318
+ # unless `record_dependencies:` was set). The rows are value objects of frozen Strings, so they marshal back.
319
+ def drain_analysis_reads
320
+ @analysis_reads || {}
321
+ end
322
+
311
323
  # ADR-103 WD12 — the per-file {Effects::FileCollection}s this session collected (empty unless the
312
324
  # configuration carries an `effects:` block). Marshal-clean by construction — frozen Hashes of
313
325
  # Strings, `LabelSet`s of frozen Arrays, and `Data` edges — so the fork pool ships them back to the
@@ -336,6 +348,45 @@ module Rigor
336
348
  end
337
349
  end
338
350
 
351
+ # Issue #1900 — `{plugin id => refused-read count}` over `registry`'s boundaries right now: the baseline
352
+ # a later drain subtracts, so it reports only the refusals made after the baseline was taken.
353
+ def self.refusal_counts(registry)
354
+ registry.plugins.to_h do |plugin|
355
+ summary = begin
356
+ plugin.io_boundary.refusal_summary
357
+ rescue StandardError
358
+ nil
359
+ end
360
+ [safe_plugin_id(plugin), summary ? summary[:count] : 0]
361
+ end
362
+ end
363
+
364
+ def self.safe_plugin_id(plugin)
365
+ plugin.manifest.id
366
+ rescue StandardError
367
+ plugin.class.to_s
368
+ end
369
+
370
+ # Issue #1900 — the {Plugin::IoBoundary} trust-policy refusals this session's plugin instances made
371
+ # since the previous drain, one `{plugin_id:, count:, first_path:, nearest_root:}` per plugin that hit
372
+ # any, in registry order. Unlike {#drain_run_disclosures} each drain reports only what is new — the
373
+ # boundary keeps its running total and this session remembers how much of it was already reported —
374
+ # because the coordinator SUMS the drains: a fork child inherits the parent's `#prepare` refusals,
375
+ # and reporting them again would count them once per worker. `first_path` is the boundary's first
376
+ # refusal overall, which is the delta's own first path whenever nothing was reported before it, and
377
+ # the coordinator keeps the first-merged path otherwise. It is not baselined after `#init`, so a plugin
378
+ # whose `#init` refuses differently here than on the coordinator registry can have this session's
379
+ # `#init` path named. Marshal-clean and Ractor-copyable.
380
+ def drain_trust_refusals
381
+ return [] if @plugin_registry.empty?
382
+
383
+ @plugin_registry.plugins.filter_map do |plugin|
384
+ trust_refusal_delta(plugin)
385
+ rescue StandardError
386
+ nil
387
+ end
388
+ end
389
+
339
390
  private
340
391
 
341
392
  # Mirrors {Runner#seed_project_scope}: applies the cross-file pre-pass discovery tables the
@@ -402,6 +453,10 @@ module Rigor
402
453
  def run_plugin_prepare
403
454
  return [] if @plugin_registry.empty?
404
455
 
456
+ # Issue #1900 — per plugin id, how much of its boundary's count is already reported. Starts at what
457
+ # `#init` refused while the registry was materialised: `#init` runs on the coordinator-side registry
458
+ # too, which reports those reads itself, so counting them here as well would double them.
459
+ @reported_refusal_counts = self.class.refusal_counts(@plugin_registry)
405
460
  @plugin_registry.plugins.flat_map do |plugin|
406
461
  plugin.prepare(plugin.services)
407
462
  []
@@ -490,6 +545,18 @@ module Rigor
490
545
  )
491
546
  end
492
547
 
548
+ def trust_refusal_delta(plugin)
549
+ summary = plugin.io_boundary.refusal_summary
550
+ return nil if summary.nil?
551
+
552
+ plugin_id = safe_plugin_id(plugin)
553
+ delta = summary[:count] - @reported_refusal_counts.fetch(plugin_id, 0)
554
+ return nil unless delta.positive?
555
+
556
+ @reported_refusal_counts[plugin_id] = summary[:count]
557
+ summary.merge(count: delta, plugin_id: plugin_id)
558
+ end
559
+
493
560
  def safe_plugin_id(plugin)
494
561
  plugin.manifest.id
495
562
  rescue StandardError