rigortype 0.2.9 → 0.3.1
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.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/builtins/ruby_core/array.yml +416 -392
- data/data/builtins/ruby_core/file.yml +42 -42
- data/data/builtins/ruby_core/hash.yml +302 -302
- data/data/builtins/ruby_core/io.yml +191 -191
- data/data/builtins/ruby_core/numeric.yml +321 -366
- data/data/builtins/ruby_core/proc.yml +124 -124
- data/data/builtins/ruby_core/range.yml +21 -21
- data/data/builtins/ruby_core/rational.yml +39 -39
- data/data/builtins/ruby_core/re.yml +65 -65
- data/data/builtins/ruby_core/set.yml +106 -106
- data/data/builtins/ruby_core/struct.yml +14 -14
- data/data/core_overlay/string_scanner.rbs +6 -5
- data/docs/handbook/01-getting-started.md +22 -34
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +19 -10
- data/docs/handbook/07-rbs-and-extended.md +76 -101
- data/docs/handbook/08-understanding-errors.md +114 -246
- data/docs/handbook/09-plugins.md +54 -144
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/README.md +5 -3
- data/docs/handbook/appendix-liskov.md +4 -2
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/handbook/appendix-phpstan.md +14 -7
- data/docs/handbook/appendix-steep.md +4 -2
- data/docs/handbook/appendix-type-theory.md +3 -1
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +98 -4
- data/docs/manual/02-cli-reference.md +81 -12
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +59 -4
- data/docs/manual/06-baseline.md +35 -1
- data/docs/manual/07-plugins.md +4 -4
- data/docs/manual/08-skills.md +6 -1
- data/docs/manual/09-editor-integration.md +7 -6
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/docs/manual/plugins/rigor-actioncable.md +32 -0
- data/docs/manual/plugins/rigor-devise.md +4 -2
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +112 -0
- data/lib/rigor/analysis/check_rules.rb +581 -98
- data/lib/rigor/analysis/dependency_recorder.rb +93 -9
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +456 -51
- data/lib/rigor/analysis/path_expansion.rb +42 -0
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
- data/lib/rigor/analysis/rule_catalog.rb +226 -1
- data/lib/rigor/analysis/run_cache_key.rb +78 -0
- data/lib/rigor/analysis/run_cache_probe.rb +72 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
- data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
- data/lib/rigor/analysis/runner.rb +344 -68
- data/lib/rigor/analysis/severity_stamp.rb +43 -0
- data/lib/rigor/analysis/worker_session.rb +30 -2
- data/lib/rigor/bleeding_edge.rb +37 -8
- data/lib/rigor/builtins/regex_refinement.rb +36 -1
- data/lib/rigor/cache/descriptor.rb +88 -28
- data/lib/rigor/cache/file_digest.rb +158 -0
- data/lib/rigor/cache/incremental_snapshot.rb +41 -5
- data/lib/rigor/cache/rbs_cache_producer.rb +11 -1
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
- data/lib/rigor/cache/store.rb +132 -27
- data/lib/rigor/ci_detector.rb +90 -0
- data/lib/rigor/cli/annotate_command.rb +15 -9
- data/lib/rigor/cli/baseline_command.rb +4 -1
- data/lib/rigor/cli/check_command.rb +223 -32
- data/lib/rigor/cli/check_invocation.rb +84 -0
- data/lib/rigor/cli/coverage_command.rb +6 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +169 -31
- data/lib/rigor/cli/lsp_command.rb +5 -0
- data/lib/rigor/cli/mcp_command.rb +5 -0
- data/lib/rigor/cli/plugins_command.rb +36 -6
- data/lib/rigor/cli/plugins_renderer.rb +44 -12
- data/lib/rigor/cli/probe_environment.rb +85 -0
- data/lib/rigor/cli/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +22 -33
- data/lib/rigor/cli/skill_deep_probe.rb +172 -0
- data/lib/rigor/cli/skill_describe.rb +75 -9
- data/lib/rigor/cli/trace_command.rb +5 -6
- data/lib/rigor/cli/type_of_command.rb +9 -8
- data/lib/rigor/cli/type_scan_command.rb +8 -8
- data/lib/rigor/cli.rb +15 -4
- data/lib/rigor/config_audit.rb +48 -2
- data/lib/rigor/configuration/severity_profile.rb +31 -0
- data/lib/rigor/configuration.rb +156 -8
- data/lib/rigor/environment/default_libraries.rb +36 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +29 -28
- data/lib/rigor/inference/budget_trace.rb +77 -2
- data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
- data/lib/rigor/inference/def_handle.rb +23 -0
- data/lib/rigor/inference/def_node_resolver.rb +90 -0
- data/lib/rigor/inference/def_return_typer.rb +2 -1
- data/lib/rigor/inference/expression_typer.rb +361 -113
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +17 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
- data/lib/rigor/inference/method_dispatcher.rb +57 -1
- data/lib/rigor/inference/mutation_widening.rb +42 -8
- data/lib/rigor/inference/narrowing.rb +127 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +8 -1
- data/lib/rigor/inference/scope_indexer.rb +451 -36
- data/lib/rigor/inference/statement_evaluator.rb +64 -15
- data/lib/rigor/inference/struct_fold_safety.rb +48 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
- data/lib/rigor/inference/void_origin.rb +25 -0
- data/lib/rigor/inference/void_tail_summary.rb +220 -0
- data/lib/rigor/language_server/buffer_resolution.rb +6 -3
- data/lib/rigor/language_server/buffer_table.rb +46 -6
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -0
- data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
- data/lib/rigor/language_server/folding_range_provider.rb +2 -1
- data/lib/rigor/language_server/incremental_sync.rb +159 -0
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/server.rb +19 -9
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/language_server.rb +1 -0
- data/lib/rigor/plugin/base.rb +38 -24
- data/lib/rigor/plugin/inflector.rb +12 -3
- data/lib/rigor/plugin/io_boundary.rb +5 -1
- data/lib/rigor/plugin/isolation.rb +81 -11
- data/lib/rigor/plugin/load_error.rb +10 -1
- data/lib/rigor/plugin/loader.rb +96 -14
- data/lib/rigor/plugin/node_rule_walk.rb +5 -3
- data/lib/rigor/plugin/registry.rb +32 -23
- data/lib/rigor/plugin.rb +26 -0
- data/lib/rigor/protection/mutator.rb +3 -2
- data/lib/rigor/reflection.rb +64 -0
- data/lib/rigor/runtime/jit.rb +128 -0
- data/lib/rigor/scope/discovery_index.rb +12 -2
- data/lib/rigor/scope.rb +122 -28
- data/lib/rigor/sig_gen/classification.rb +5 -1
- data/lib/rigor/sig_gen/generator.rb +38 -6
- data/lib/rigor/sig_gen/observation_collector.rb +4 -3
- data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
- data/lib/rigor/sig_gen/renderer.rb +10 -0
- data/lib/rigor/sig_gen/write_result.rb +9 -4
- data/lib/rigor/sig_gen/writer.rb +207 -49
- data/lib/rigor/source/node_children.rb +116 -0
- data/lib/rigor/source/node_locator.rb +3 -1
- data/lib/rigor/source/node_walker.rb +4 -2
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/difference.rb +28 -24
- data/lib/rigor/type/hash_shape.rb +34 -10
- data/lib/rigor/version.rb +1 -1
- data/lib/rigortype.rb +24 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +51 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +7 -5
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
- data/sig/prism_node_children.rbs +9 -0
- data/sig/rigor/cache.rbs +6 -0
- data/sig/rigor/inference/void_origin.rbs +18 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +6 -6
- data/sig/rigor/reflection.rbs +1 -0
- data/sig/rigor/scope.rbs +12 -1
- data/skills/rigor-ci-setup/SKILL.md +10 -0
- data/skills/rigor-editor-setup/SKILL.md +11 -0
- data/skills/rigor-next-steps/SKILL.md +23 -2
- data/skills/rigor-plugin-author/SKILL.md +3 -3
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
- data/skills/rigor-plugin-review/SKILL.md +2 -2
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
- data/skills/rigor-project-init/SKILL.md +29 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -1
- data/skills/rigor-project-init/references/02-configure.md +39 -5
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
- data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
- metadata +48 -26
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -9,11 +9,13 @@ require_relative "../configuration"
|
|
|
9
9
|
require_relative "options"
|
|
10
10
|
require_relative "../environment"
|
|
11
11
|
require_relative "../scope"
|
|
12
|
+
require_relative "../source/node_children"
|
|
12
13
|
require_relative "../inference/def_return_typer"
|
|
13
14
|
require_relative "../inference/scope_indexer"
|
|
14
15
|
require_relative "../inference/statement_evaluator"
|
|
15
16
|
require_relative "prism_colorizer"
|
|
16
17
|
require_relative "command"
|
|
18
|
+
require_relative "probe_environment"
|
|
17
19
|
|
|
18
20
|
module Rigor
|
|
19
21
|
class CLI
|
|
@@ -105,7 +107,7 @@ module Rigor
|
|
|
105
107
|
# bindings, so a loop-body line annotates the joined widened type (`Integer`) rather than a stale
|
|
106
108
|
# first-iterations constant (`1 | 2`).
|
|
107
109
|
scope_index = Inference::ScopeIndexer.index(
|
|
108
|
-
parse_result.value, default_scope: base_scope(configuration),
|
|
110
|
+
parse_result.value, default_scope: base_scope(configuration, file),
|
|
109
111
|
converged_loop_recording: true
|
|
110
112
|
)
|
|
111
113
|
line_types = LineTypeCollector.new(scope_index).collect(parse_result.value)
|
|
@@ -128,12 +130,12 @@ module Rigor
|
|
|
128
130
|
@out.puts(JSON.generate({ "annotations" => annotations }))
|
|
129
131
|
end
|
|
130
132
|
|
|
131
|
-
|
|
133
|
+
# The plugin-aware environment for the annotated file, so a `#=> <type>` matches what `rigor check`
|
|
134
|
+
# infers on that line — including types synthesized from the file's own inline RBS annotations (the file
|
|
135
|
+
# is threaded as the synthesizer's `source_files:`; see {ProbeEnvironment}).
|
|
136
|
+
def base_scope(configuration, file)
|
|
132
137
|
Scope.empty(
|
|
133
|
-
environment:
|
|
134
|
-
libraries: configuration.libraries,
|
|
135
|
-
signature_paths: configuration.signature_paths
|
|
136
|
-
)
|
|
138
|
+
environment: ProbeEnvironment.build(configuration: configuration, source_files: [file])
|
|
137
139
|
)
|
|
138
140
|
end
|
|
139
141
|
|
|
@@ -247,14 +249,18 @@ module Rigor
|
|
|
247
249
|
block.call(stmt)
|
|
248
250
|
end
|
|
249
251
|
else
|
|
250
|
-
node.
|
|
252
|
+
node.rigor_each_child { |child| each_statement(child, &block) }
|
|
251
253
|
end
|
|
252
254
|
end
|
|
253
255
|
|
|
254
256
|
# For a line no statement closes (the `if` / block header lines), fall back to the widest expression ending there.
|
|
257
|
+
# A hash-pair node (`1 => 2,` inside a multi-line literal / keyword-argument list) is not an expression — typing
|
|
258
|
+
# it only echoes the `Dynamic[top]` fallback — so those interior lines stay bare and the literal's type appears
|
|
259
|
+
# once, on the line the enclosing statement closes.
|
|
255
260
|
def fill_uncovered_lines(program, by_line)
|
|
256
261
|
widest_per_line(program).each do |line, node|
|
|
257
262
|
next if by_line.key?(line)
|
|
263
|
+
next if node.is_a?(Prism::AssocNode) || node.is_a?(Prism::AssocSplatNode)
|
|
258
264
|
|
|
259
265
|
type = node.is_a?(Prism::BlockParametersNode) ? block_params_type(node) : type_of(node)
|
|
260
266
|
by_line[line] = type unless type.nil?
|
|
@@ -298,7 +304,7 @@ module Rigor
|
|
|
298
304
|
return if node.nil?
|
|
299
305
|
|
|
300
306
|
block.call(node)
|
|
301
|
-
node.
|
|
307
|
+
node.rigor_each_child { |child| walk(child, &block) }
|
|
302
308
|
end
|
|
303
309
|
|
|
304
310
|
# Types the node through the flow evaluator (not the bare expression typer) under its recorded entry scope, so
|
|
@@ -331,7 +337,7 @@ module Rigor
|
|
|
331
337
|
return if node.nil?
|
|
332
338
|
|
|
333
339
|
block.call(node) if node.is_a?(Prism::DefNode)
|
|
334
|
-
node.
|
|
340
|
+
node.rigor_each_child { |child| each_def_node(child, &block) }
|
|
335
341
|
end
|
|
336
342
|
end
|
|
337
343
|
end
|
|
@@ -87,8 +87,11 @@ module Rigor
|
|
|
87
87
|
"match-mode: #{options.fetch(:match_mode)})"
|
|
88
88
|
)
|
|
89
89
|
if configuration.baseline_path.nil?
|
|
90
|
+
# Name the config file that actually governs this project (`.rigor.dist.yml` when no local override
|
|
91
|
+
# exists), so the hint points at the file the user must edit.
|
|
92
|
+
config_name = options.fetch(:config) || Configuration.discover || ".rigor.yml"
|
|
90
93
|
@err.puts(
|
|
91
|
-
"rigor: note —
|
|
94
|
+
"rigor: note — `#{config_name}` does not declare `baseline:`; " \
|
|
92
95
|
"add `baseline: #{path}` to activate the suppression."
|
|
93
96
|
)
|
|
94
97
|
end
|
|
@@ -8,12 +8,17 @@ require_relative "../configuration"
|
|
|
8
8
|
require_relative "../config_audit"
|
|
9
9
|
require_relative "../analysis/result"
|
|
10
10
|
require_relative "../analysis/rule_catalog"
|
|
11
|
-
|
|
11
|
+
# The baseline filter runs on EVERY exit path — including the ADR-87 WD4 cache-HIT fast path, which skips
|
|
12
|
+
# `load_check_dependencies` — so it must be a load-time require. It pulls only YAML, never the engine.
|
|
13
|
+
require_relative "../analysis/baseline"
|
|
14
|
+
require_relative "../runtime/jit"
|
|
12
15
|
require_relative "command"
|
|
13
16
|
require_relative "options"
|
|
14
17
|
require_relative "diagnostic_formats"
|
|
15
|
-
require_relative "ci_detector"
|
|
16
|
-
|
|
18
|
+
require_relative "../ci_detector"
|
|
19
|
+
# ADR-87 WD4 — `coverage_scan` and `check_runner_factory` both pull the inference engine, so they are required
|
|
20
|
+
# lazily (in `compute_coverage` / `build_check_runner`) rather than at load time: an ordinary check whose
|
|
21
|
+
# result the run-cache probe serves must reach the hit verdict without `rigor/inference` in $LOADED_FEATURES.
|
|
17
22
|
|
|
18
23
|
module Rigor
|
|
19
24
|
class CLI
|
|
@@ -29,26 +34,43 @@ module Rigor
|
|
|
29
34
|
# concerns that are clearer read together than split across micro-classes.
|
|
30
35
|
class CheckCommand < Command # rubocop:disable Metrics/ClassLength
|
|
31
36
|
# @return [Integer] CLI exit status.
|
|
32
|
-
def run # rubocop:disable Metrics/AbcSize
|
|
33
|
-
|
|
37
|
+
def run # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
38
|
+
# Arm deferred YJIT enablement before any analysis work: the deadline
|
|
39
|
+
# thread only fires once a run outlasts the amortization window, so a
|
|
40
|
+
# short check finishes before it ever pays JIT compile cost while a
|
|
41
|
+
# long run JITs its dominant tail (Runtime::Jit).
|
|
42
|
+
Runtime::Jit.enable_after(Runtime::Jit.deadline_seconds)
|
|
43
|
+
# ADR-87 WD4 — parse options + resolve config WITHOUT the inference engine, so the run-cache hit probe
|
|
44
|
+
# can run first. The heavy engine (`load_check_dependencies`) loads only on a miss / non-cacheable run.
|
|
34
45
|
options = parse_check_options
|
|
35
46
|
buffer = Options.resolve_buffer_binding(options, err: @err)
|
|
36
47
|
return CLI::EXIT_USAGE if buffer == :usage_error
|
|
37
48
|
|
|
38
49
|
configuration = load_check_configuration(options)
|
|
39
50
|
configuration = apply_bleeding_edge_override(configuration, options)
|
|
51
|
+
conflict = parameter_inference_incremental_conflict(configuration, options)
|
|
52
|
+
return conflict unless conflict.nil?
|
|
53
|
+
|
|
40
54
|
config_warnings = warn_unresolved_config(configuration)
|
|
41
55
|
cache_root = configuration.cache_path
|
|
42
56
|
handle_clear_cache(cache_root) if options.fetch(:clear_cache)
|
|
43
57
|
|
|
58
|
+
# ADR-87 WD4 — try to serve the whole run from the ADR-45 cache before booting the engine. A hit boots
|
|
59
|
+
# only CLI + config + cache + digest code (no `rigor/inference`), skipping the plugin prepass + env
|
|
60
|
+
# build entirely.
|
|
61
|
+
probed = try_run_cache_hit(configuration, options, buffer, cache_root)
|
|
62
|
+
return finalize_cache_hit(probed, configuration, options, config_warnings) unless probed.nil?
|
|
63
|
+
|
|
64
|
+
load_check_dependencies
|
|
44
65
|
special = dispatch_special_check_mode(configuration, options, cache_root)
|
|
45
66
|
return special unless special.nil?
|
|
46
67
|
|
|
47
|
-
|
|
68
|
+
invocation = invoke_check(
|
|
48
69
|
configuration: configuration, options: options,
|
|
49
70
|
buffer: buffer, cache_root: cache_root
|
|
50
71
|
)
|
|
51
|
-
|
|
72
|
+
runner = invocation.runner
|
|
73
|
+
raw_result = invocation.result
|
|
52
74
|
result = apply_baseline_filter(raw_result, configuration, options)
|
|
53
75
|
|
|
54
76
|
coverage = compute_coverage(runner, configuration, options)
|
|
@@ -66,23 +88,113 @@ module Rigor
|
|
|
66
88
|
|
|
67
89
|
private
|
|
68
90
|
|
|
91
|
+
# ADR-87 WD4 — attempt the boot-slimming run-cache hit. Returns the cached {Analysis::Result} (severity
|
|
92
|
+
# profile applied, no stats — matching a cache-served `Runner#run`) on a hit, or nil to fall through to
|
|
93
|
+
# the full engine path. Only an ordinary sequential check whose result IS cache-served the same way is
|
|
94
|
+
# eligible; every other mode (below) declines so the full path handles it unchanged.
|
|
95
|
+
def try_run_cache_hit(configuration, options, buffer, cache_root)
|
|
96
|
+
return nil unless run_cache_hit_eligible?(configuration, options, buffer)
|
|
97
|
+
|
|
98
|
+
require_relative "../analysis/run_cache_probe"
|
|
99
|
+
Analysis::RunCacheProbe.new(
|
|
100
|
+
configuration: configuration, cache_root: cache_root, explain: options.fetch(:explain)
|
|
101
|
+
).serve(@argv.empty? ? configuration.paths : @argv)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The probe is eligible only for a run the ADR-45 result cache actually serves: a writable cache (not
|
|
105
|
+
# `--no-cache`), no editor buffer, sequential (pool runs are not result-cacheable), and no mode needing
|
|
106
|
+
# the engine (`--coverage` / `--incremental` / `--verify-incremental`) or the runner's own reporting
|
|
107
|
+
# (`--cache-stats`, the `RIGOR_*_TRACE` dev probes). A wrongly-permitted run still MISSES (the entry was
|
|
108
|
+
# never written its way) and falls through — the gate is an optimisation, never a soundness boundary.
|
|
109
|
+
def run_cache_hit_eligible?(configuration, options, buffer)
|
|
110
|
+
buffer.nil? &&
|
|
111
|
+
!options.fetch(:no_cache) && !options.fetch(:coverage) && !options.fetch(:cache_stats) &&
|
|
112
|
+
!options.fetch(:incremental) && !options.fetch(:verify_incremental) &&
|
|
113
|
+
!pool_workers_configured?(options, configuration) &&
|
|
114
|
+
ENV["RIGOR_BUDGET_TRACE"].to_s.empty? && ENV["RIGOR_HEAP_TRACE"].to_s.empty?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Any signal that per-file analysis would run in a worker pool (CLI `--workers`, the env override, or the
|
|
118
|
+
# config default). Deliberately over-declines (a false positive only forgoes the fast lane); mirrors
|
|
119
|
+
# {CheckRunnerFactory.resolve_workers} without requiring it (that pulls in the engine).
|
|
120
|
+
def pool_workers_configured?(options, configuration)
|
|
121
|
+
cli = options[:workers]
|
|
122
|
+
return Integer(cli).positive? if cli
|
|
123
|
+
|
|
124
|
+
env = ENV.fetch("RIGOR_RACTOR_WORKERS", nil)
|
|
125
|
+
return Integer(env).positive? if env && !env.empty?
|
|
126
|
+
|
|
127
|
+
configuration.parallel_workers.positive?
|
|
128
|
+
rescue ArgumentError
|
|
129
|
+
false
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# ADR-87 WD4 — the engine-free tail for a cache-served hit: baseline filter + output + exit code,
|
|
133
|
+
# mirroring the full path's tail but without stats (a hit collected none), trace appendices (they read
|
|
134
|
+
# engine counters), or eviction (deferred to the next miss). `raw_result` is pre-baseline so the strict
|
|
135
|
+
# gate reads the same diagnostics the full path would.
|
|
136
|
+
def finalize_cache_hit(raw_result, configuration, options, config_warnings)
|
|
137
|
+
result = apply_baseline_filter(raw_result, configuration, options)
|
|
138
|
+
write_result(result, options.fetch(:format), config_warnings: config_warnings)
|
|
139
|
+
emit_ci_detected_output(result, options)
|
|
140
|
+
|
|
141
|
+
exit_code = result.success? ? 0 : 1
|
|
142
|
+
exit_code = 1 if baseline_strict_violation?(raw_result.diagnostics, configuration, options)
|
|
143
|
+
exit_code
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# ADR-67 WD6c — `parameter_inference:` and the ADR-46 incremental modes are mutually exclusive in slice 1.
|
|
147
|
+
# The parameter table introduces cross-file edges (a caller's argument types drive a callee's body
|
|
148
|
+
# diagnostics) that the per-file dependency recorder does not yet carry, so a cached file whose parameter
|
|
149
|
+
# seeds changed would serve a stale result. Refuse with a message naming the ADR-46 gap rather than
|
|
150
|
+
# silently producing unsound incremental diagnostics; the edge wiring is the named follow-up. Returns the
|
|
151
|
+
# usage exit code on a conflict, or nil to proceed. `--verify-incremental` (the acceptance gate that runs
|
|
152
|
+
# incremental analysis) is caught here too.
|
|
153
|
+
def parameter_inference_incremental_conflict(configuration, options)
|
|
154
|
+
return nil unless configuration.parameter_inference
|
|
155
|
+
return nil unless options.fetch(:incremental) || options.fetch(:verify_incremental)
|
|
156
|
+
|
|
157
|
+
flag = options.fetch(:incremental) ? "--incremental" : "--verify-incremental"
|
|
158
|
+
@err.puts("rigor: parameter_inference: cannot combine with #{flag} — the call-site parameter table " \
|
|
159
|
+
"introduces cross-file caller→callee edges the ADR-46 incremental dependency graph does not " \
|
|
160
|
+
"yet record, so a cached file could serve a stale diagnostic. Run a full check (drop " \
|
|
161
|
+
"#{flag}), or disable parameter_inference: for this run.")
|
|
162
|
+
CLI::EXIT_USAGE
|
|
163
|
+
end
|
|
164
|
+
|
|
69
165
|
# ADR-46 — the two incremental-analysis check modes both fully handle the run and return an exit code (so `run`
|
|
70
166
|
# short-circuits); returns nil for an ordinary check.
|
|
71
167
|
def dispatch_special_check_mode(configuration, options, cache_root)
|
|
72
|
-
return run_verify_incremental(configuration) if options.fetch(:verify_incremental)
|
|
168
|
+
return run_verify_incremental(configuration, options, cache_root) if options.fetch(:verify_incremental)
|
|
73
169
|
return run_incremental_check(configuration, options, cache_root) if options.fetch(:incremental)
|
|
74
170
|
|
|
75
171
|
nil
|
|
76
172
|
end
|
|
77
173
|
|
|
174
|
+
# ADR-85 WD1 — the persistent cache the incremental session threads into each internal Runner so plugin
|
|
175
|
+
# `#prepare` producers (the ADR-9/#74/ADR-60 WD3 record-and-validate caches) and the RBS environment serve
|
|
176
|
+
# from disk across processes / rechecks rather than recomputing every invocation. Honors `--no-cache` (nil
|
|
177
|
+
# = no store), mirroring {CheckRunnerFactory}. The whole-run ADR-45 result cache stays inert on these runs
|
|
178
|
+
# — `Runner#run_result_cacheable?` excludes `record_dependencies` / `analyze_only`.
|
|
179
|
+
def incremental_cache_store(configuration, options, cache_root)
|
|
180
|
+
return nil if options.fetch(:no_cache)
|
|
181
|
+
|
|
182
|
+
Cache::Store.new(root: cache_root, max_bytes: configuration.cache_max_bytes)
|
|
183
|
+
end
|
|
184
|
+
|
|
78
185
|
# ADR-46 — the incremental-analysis acceptance gate. Runs a baseline analysis (recording cross-file dependencies),
|
|
79
186
|
# then re-analyzes a representative subset of files and serves the rest from the per-file cache (the body tier),
|
|
80
187
|
# and asserts the merged diagnostics are byte-identical to a full `--no-cache` analysis. A mismatch means the
|
|
81
188
|
# incremental machinery would serve a stale — manufactured — diagnostic, the soundness failure this gate exists to
|
|
82
189
|
# catch. Prints a one-line PASS (exit 0) or the differing diagnostics (exit 1).
|
|
83
|
-
def run_verify_incremental(configuration)
|
|
190
|
+
def run_verify_incremental(configuration, options, cache_root)
|
|
84
191
|
paths = @argv.empty? ? nil : @argv
|
|
85
|
-
|
|
192
|
+
# ADR-85 WD1 — thread the store so the gate also asserts the cache-served producer path stays
|
|
193
|
+
# byte-identical to the uncached full run (`verify_full_diagnostics` builds a nil-store runner).
|
|
194
|
+
session = Analysis::IncrementalSession.new(
|
|
195
|
+
configuration: configuration, paths: paths,
|
|
196
|
+
cache_store: incremental_cache_store(configuration, options, cache_root)
|
|
197
|
+
)
|
|
86
198
|
session.baseline
|
|
87
199
|
analyzed = session.analyzed_files
|
|
88
200
|
|
|
@@ -101,18 +213,30 @@ module Rigor
|
|
|
101
213
|
# updated snapshot for the next invocation. Diagnostics are identical to a full run (the `--verify-incremental`
|
|
102
214
|
# gate enforces this); the win is skipping per-file inference for unchanged files.
|
|
103
215
|
def run_incremental_check(configuration, options, cache_root)
|
|
216
|
+
require_relative "check_runner_factory"
|
|
104
217
|
paths = @argv.empty? ? nil : @argv
|
|
105
|
-
probe = Analysis::Runner.new(configuration: configuration, cache_store: nil)
|
|
106
|
-
files = paths ? probe.analysis_file_set(paths) : probe.analysis_file_set
|
|
107
218
|
fingerprint = Cache::IncrementalSnapshot.fingerprint(
|
|
108
219
|
configuration: configuration, roots: paths || configuration.paths
|
|
109
220
|
)
|
|
110
221
|
snapshot = Cache::IncrementalSnapshot.new(root: cache_root)
|
|
111
|
-
|
|
222
|
+
store = incremental_cache_store(configuration, options, cache_root)
|
|
223
|
+
session = Analysis::IncrementalSession.new(
|
|
224
|
+
configuration: configuration, paths: paths,
|
|
225
|
+
cache_store: store,
|
|
226
|
+
# ADR-46 — thread the same worker-count precedence the standard `check` path uses
|
|
227
|
+
# (CLI `--workers` > `RIGOR_RACTOR_WORKERS` > `parallel.workers:` > 0) so the recheck's closure
|
|
228
|
+
# re-analysis parallelises; the fork pool marshals dependency records back so the graph is sound.
|
|
229
|
+
workers: CheckRunnerFactory.resolve_workers(options, configuration)
|
|
230
|
+
)
|
|
112
231
|
|
|
113
232
|
diagnostics, warm = session.run_incremental(snapshot: snapshot, fingerprint: fingerprint)
|
|
233
|
+
# The banner's file count comes from the session's analyzed set (cold analyses all; a warm recheck's
|
|
234
|
+
# `@analyzed` advances to the current file set), so a dedicated probe Runner + `Dir.glob` is no longer
|
|
235
|
+
# built just to size the banner (recon §3 — the analysis tree was expanded three times per recheck).
|
|
114
236
|
@err.puts("rigor: --incremental #{warm ? 'warm — reused cached diagnostics' : 'cold — full analysis'} " \
|
|
115
|
-
"(#{
|
|
237
|
+
"(#{session.analyzed_files.size} files)")
|
|
238
|
+
emit_incremental_fact_surface_notes(session)
|
|
239
|
+
write_incremental_cache_stats(session, cache_root, store) if options.fetch(:cache_stats)
|
|
116
240
|
|
|
117
241
|
result = apply_baseline_filter(Analysis::Result.new(diagnostics: diagnostics, stats: nil), configuration,
|
|
118
242
|
options)
|
|
@@ -120,6 +244,40 @@ module Rigor
|
|
|
120
244
|
result.success? ? 0 : 1
|
|
121
245
|
end
|
|
122
246
|
|
|
247
|
+
# ADR-88 WD1 — a one-line stderr note when the plugin fact surface (an ADR-9 fact, an ADR-60 producer
|
|
248
|
+
# value, or an `incremental_state_fingerprint` hook) forced a full run: either it CHANGED since the
|
|
249
|
+
# snapshot (a Sorbet sig edit, a schema change) or a contributing plugin declares NO fingerprint surface
|
|
250
|
+
# (opaque — a full run every invocation). Both are the sound direction; the note explains why a recheck
|
|
251
|
+
# was not warm.
|
|
252
|
+
def emit_incremental_fact_surface_notes(session)
|
|
253
|
+
opaque = session.opaque_plugin_ids
|
|
254
|
+
unless opaque.empty?
|
|
255
|
+
@err.puts("rigor: --incremental plugin#{'s' unless opaque.size == 1} #{opaque.sort.join(', ')} " \
|
|
256
|
+
"contribute call-site types with no incremental fingerprint surface; the snapshot cannot " \
|
|
257
|
+
"be reused (full analysis every run).")
|
|
258
|
+
end
|
|
259
|
+
return unless session.fact_surface_invalidated? && opaque.empty?
|
|
260
|
+
|
|
261
|
+
@err.puts("rigor: --incremental plugin fact surface changed since the snapshot; ran a full analysis.")
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# ADR-88 WD1 — `--incremental --cache-stats` surfaces the on-disk cache inventory (as a plain `check`
|
|
265
|
+
# does) plus the fact-surface status line, so an operator can see whether a full run was fact-surface
|
|
266
|
+
# driven. The plain `check` cache-stats path is bypassed by the incremental short-circuit, so it is
|
|
267
|
+
# emitted here.
|
|
268
|
+
def write_incremental_cache_stats(session, cache_root, store)
|
|
269
|
+
write_cache_stats(cache_root, store)
|
|
270
|
+
status =
|
|
271
|
+
if !session.opaque_plugin_ids.empty?
|
|
272
|
+
"opaque (#{session.opaque_plugin_ids.sort.join(', ')})"
|
|
273
|
+
elsif session.fact_surface_invalidated?
|
|
274
|
+
"changed — snapshot invalidated"
|
|
275
|
+
else
|
|
276
|
+
"unchanged"
|
|
277
|
+
end
|
|
278
|
+
@out.puts(" plugin fact surface: #{status}")
|
|
279
|
+
end
|
|
280
|
+
|
|
123
281
|
def verify_full_diagnostics(configuration, paths)
|
|
124
282
|
runner = Analysis::Runner.new(configuration: configuration, cache_store: nil)
|
|
125
283
|
(paths ? runner.run(paths) : runner.run).diagnostics
|
|
@@ -226,10 +384,16 @@ module Rigor
|
|
|
226
384
|
@err.puts("rigor: #{silenced_count} diagnostic(s) silenced by baseline #{baseline_path}")
|
|
227
385
|
end
|
|
228
386
|
|
|
229
|
-
|
|
230
|
-
|
|
387
|
+
# The primary check run, routed through the shared {CheckInvocation} entry point so `rigor doctor` and
|
|
388
|
+
# `rigor skill describe --deep` reach a result through this exact path rather than re-deriving it (#148).
|
|
389
|
+
# Required lazily for the reason the old inline `CheckRunnerFactory.build` was: it pulls the inference engine,
|
|
390
|
+
# and ADR-87 WD4's cache-hit fast path must reach its verdict without it.
|
|
391
|
+
def invoke_check(configuration:, options:, buffer:, cache_root:)
|
|
392
|
+
require_relative "check_invocation"
|
|
393
|
+
CheckInvocation.run(
|
|
231
394
|
configuration: configuration, options: options,
|
|
232
|
-
buffer: buffer, cache_root: cache_root
|
|
395
|
+
buffer: buffer, cache_root: cache_root,
|
|
396
|
+
paths: @argv.empty? ? nil : @argv
|
|
233
397
|
)
|
|
234
398
|
end
|
|
235
399
|
|
|
@@ -391,7 +555,10 @@ module Rigor
|
|
|
391
555
|
end
|
|
392
556
|
|
|
393
557
|
def inject_treat_all_as_inline_rbs(entries)
|
|
394
|
-
|
|
558
|
+
# Single-homed with the WD2 auto-wire gate (`Configuration.rbs_inline_plugin_entry?`) so the flag and
|
|
559
|
+
# the default agree on what "already listed" means; a pre-existing entry is removed so this one's
|
|
560
|
+
# `require_magic_comment: false` wins unconditionally.
|
|
561
|
+
filtered = entries.reject { |entry| Configuration.rbs_inline_plugin_entry?(entry) }
|
|
395
562
|
filtered + [{
|
|
396
563
|
"gem" => "rigor-rbs-inline",
|
|
397
564
|
"id" => "rbs-inline",
|
|
@@ -399,18 +566,6 @@ module Rigor
|
|
|
399
566
|
}]
|
|
400
567
|
end
|
|
401
568
|
|
|
402
|
-
def rigor_rbs_inline_entry?(entry)
|
|
403
|
-
case entry
|
|
404
|
-
when String
|
|
405
|
-
entry == "rigor-rbs-inline"
|
|
406
|
-
when Hash
|
|
407
|
-
string_keyed = entry.to_h { |k, v| [k.to_s, v] }
|
|
408
|
-
string_keyed["gem"] == "rigor-rbs-inline" || string_keyed["id"] == "rbs-inline"
|
|
409
|
-
else
|
|
410
|
-
false
|
|
411
|
-
end
|
|
412
|
-
end
|
|
413
|
-
|
|
414
569
|
def handle_clear_cache(cache_root)
|
|
415
570
|
if File.directory?(cache_root)
|
|
416
571
|
FileUtils.rm_rf(cache_root)
|
|
@@ -451,6 +606,42 @@ module Rigor
|
|
|
451
606
|
@err.puts(" recursion-fixpoint-cap hits: #{counts[Inference::BudgetTrace::RECURSION_FIXPOINT_CAP]}")
|
|
452
607
|
@err.puts(" block-writeback-cap hits: #{counts[Inference::BudgetTrace::BLOCK_WRITEBACK_CAP]}")
|
|
453
608
|
write_budget_distributions
|
|
609
|
+
write_memo_profile(counts)
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
# Dumps the ADR-57 return-memo profile (WD0). Entries / hit rate / body evals / the non-store split, then
|
|
613
|
+
# the top signatures by body-eval count with their distinct-memo-key counts — the evidence for choosing
|
|
614
|
+
# between a finalization-aware taint gate and memo-key normalization. `--workers 0` for exact counts.
|
|
615
|
+
def write_memo_profile(counts)
|
|
616
|
+
bt = Inference::BudgetTrace
|
|
617
|
+
hits = counts[bt::MEMO_HITS]
|
|
618
|
+
misses = counts[bt::MEMO_MISSES]
|
|
619
|
+
consults = hits + misses
|
|
620
|
+
rate = consults.positive? ? format("%.1f%%", 100.0 * hits / consults) : "n/a"
|
|
621
|
+
@err.puts("")
|
|
622
|
+
@err.puts("Return-memo profile (RIGOR_BUDGET_TRACE; --workers 0 for an exact count)")
|
|
623
|
+
@err.puts(" infer entries: #{counts[bt::MEMO_ENTRIES]}")
|
|
624
|
+
@err.puts(" memo consults: #{consults} (hits #{hits} / misses #{misses}; hit rate #{rate})")
|
|
625
|
+
@err.puts(" body evals: #{counts[bt::MEMO_BODY_EVALS]}")
|
|
626
|
+
@err.puts(" non-stored: on-stack #{counts[bt::MEMO_REFUSE_ON_STACK]} " \
|
|
627
|
+
"unroll-in-flight #{counts[bt::MEMO_REFUSE_UNROLL]} " \
|
|
628
|
+
"consult-tainted #{counts[bt::MEMO_REFUSE_CONSULT_TAINTED]} " \
|
|
629
|
+
"transient-tainted #{counts[bt::MEMO_REFUSE_TRANSIENT]}")
|
|
630
|
+
write_memo_signature_table
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
# Top 15 signatures by body-eval (compute) count, each with its distinct-memo-key count — a signature
|
|
634
|
+
# whose distinct-key count dwarfs its evals is arg-granularity thrash (the key-normalization candidate).
|
|
635
|
+
def write_memo_signature_table
|
|
636
|
+
bt = Inference::BudgetTrace
|
|
637
|
+
evals = bt.distribution(bt::MEMO_BODY_EVAL_BY_SIGNATURE)
|
|
638
|
+
return if evals.empty?
|
|
639
|
+
|
|
640
|
+
keys = bt.distribution(bt::MEMO_DISTINCT_KEY_BY_SIGNATURE)
|
|
641
|
+
@err.puts(" top signatures by body-eval count (evals / distinct-keys / signature):")
|
|
642
|
+
evals.sort_by { |sig, n| [-n, sig] }.first(15).each do |sig, n|
|
|
643
|
+
@err.puts(" #{n.to_s.rjust(8)} #{keys.fetch(sig, 0).to_s.rjust(6)} #{sig}")
|
|
644
|
+
end
|
|
454
645
|
end
|
|
455
646
|
|
|
456
647
|
# Dumps the read-only size distributions (ADR-41 Slice 2a). These observe how large unions actually get, with no
|
|
@@ -488,7 +679,6 @@ module Rigor
|
|
|
488
679
|
def load_check_dependencies
|
|
489
680
|
require_relative "../analysis/runner"
|
|
490
681
|
require_relative "../analysis/buffer_binding"
|
|
491
|
-
require_relative "../analysis/baseline"
|
|
492
682
|
require_relative "../cache/store"
|
|
493
683
|
start_heap_trace_if_requested
|
|
494
684
|
end
|
|
@@ -625,6 +815,7 @@ module Rigor
|
|
|
625
815
|
def compute_coverage(runner, configuration, options)
|
|
626
816
|
return nil unless options.fetch(:coverage)
|
|
627
817
|
|
|
818
|
+
require_relative "coverage_scan"
|
|
628
819
|
files = @argv.empty? ? runner.analysis_file_set : runner.analysis_file_set(@argv)
|
|
629
820
|
CoverageScan.precision_report(files: files, configuration: configuration)
|
|
630
821
|
end
|
|
@@ -682,7 +873,7 @@ module Rigor
|
|
|
682
873
|
return unless options.fetch(:ci_detect)
|
|
683
874
|
return unless options.fetch(:format) == "text"
|
|
684
875
|
|
|
685
|
-
platform =
|
|
876
|
+
platform = CiDetector.detect
|
|
686
877
|
return if platform.nil?
|
|
687
878
|
|
|
688
879
|
if platform.native_stdout?
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class CLI
|
|
5
|
+
# The one place a CLI command turns a loaded {Configuration} into an {Analysis::Result}.
|
|
6
|
+
#
|
|
7
|
+
# {CheckRunnerFactory} builds the runner; this module owns the *invocation* wrapped around it — resolving the
|
|
8
|
+
# analysed paths, running, and handing back both the runner (`rigor check`'s reporting tail still needs its cache
|
|
9
|
+
# store and stats) and the raw result. It exists so a non-`check` command — `rigor doctor`, `rigor skill describe
|
|
10
|
+
# --deep` — reaches a check result through the same code path `rigor check` walks, instead of re-deriving the
|
|
11
|
+
# plumbing (issue #148; ADR-73 § "Field-trial follow-ups" names the shared helper as the prerequisite).
|
|
12
|
+
#
|
|
13
|
+
# Nothing here is required at load time: {CheckRunnerFactory} pulls the inference engine, so both entry points
|
|
14
|
+
# require it lazily. A command that merely *mentions* this module therefore stays engine-free until it actually
|
|
15
|
+
# runs an analysis — the property ADR-87 WD4 protects for `rigor check`'s cache-hit fast path, and the property
|
|
16
|
+
# that keeps an un-flagged `rigor skill describe` from loading the engine at all.
|
|
17
|
+
module CheckInvocation
|
|
18
|
+
# A completed analysis. `runner` is exposed because `rigor check`'s tail reads its `cache_store` (eviction,
|
|
19
|
+
# `--cache-stats`) and feeds it to `--coverage`; `result` is the raw, pre-baseline-filter {Analysis::Result}.
|
|
20
|
+
Invocation = Data.define(:runner, :result)
|
|
21
|
+
|
|
22
|
+
# The outcome of {.attempt} — the best-effort variant. Exactly one of the two is non-nil, so a caller can never
|
|
23
|
+
# read "the check could not run" as "the check ran clean": `result` is nil precisely when `error` is set.
|
|
24
|
+
Outcome = Data.define(:result, :error) do
|
|
25
|
+
# @return [Boolean] true when an analysis actually completed.
|
|
26
|
+
def ran?
|
|
27
|
+
!result.nil?
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Options for a caller that wants a check *result* rather than a check *run*: no cache writes, no explain
|
|
32
|
+
# traces, sequential. `stats: true` because the RBS-environment signal (`stats.rbs_classes_total`) lives there.
|
|
33
|
+
# This is `rigor doctor`'s preset — a diagnostic command that must not churn the project's cache.
|
|
34
|
+
READ_ONLY_OPTIONS = { no_cache: true, explain: false, stats: true, workers: 0 }.freeze
|
|
35
|
+
|
|
36
|
+
# Options for an opt-in deep probe (`rigor skill describe --deep`). Unlike {READ_ONLY_OPTIONS} this uses the
|
|
37
|
+
# configured cache and worker count, i.e. it behaves exactly like `rigor check` — fast on a warm cache, and it
|
|
38
|
+
# WRITES `.rigor/cache`. That side effect is the whole reason the flag is opt-in.
|
|
39
|
+
DEEP_OPTIONS = { no_cache: false, explain: false, stats: true, workers: nil }.freeze
|
|
40
|
+
|
|
41
|
+
module_function
|
|
42
|
+
|
|
43
|
+
# Runs the analysis the way `rigor check` does and returns the runner + its raw result.
|
|
44
|
+
#
|
|
45
|
+
# @param configuration [Rigor::Configuration]
|
|
46
|
+
# @param options [Hash] at least `:no_cache`, `:explain`, `:stats`, `:workers` (see {CheckRunnerFactory.build}).
|
|
47
|
+
# @param paths [Array<String>, nil] analysed paths; nil falls back to the configuration's `paths:`.
|
|
48
|
+
# @param buffer [Rigor::Analysis::BufferBinding, nil]
|
|
49
|
+
# @param cache_root [String, nil] nil falls back to the configuration's cache path.
|
|
50
|
+
# @return [Invocation]
|
|
51
|
+
def run(configuration:, options:, paths: nil, buffer: nil, cache_root: nil)
|
|
52
|
+
require_relative "check_runner_factory"
|
|
53
|
+
runner = CheckRunnerFactory.build(
|
|
54
|
+
configuration: configuration,
|
|
55
|
+
options: options,
|
|
56
|
+
buffer: buffer,
|
|
57
|
+
cache_root: cache_root || configuration.cache_path
|
|
58
|
+
)
|
|
59
|
+
Invocation.new(runner: runner, result: runner.run(paths || configuration.paths))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Best-effort variant for a *routing* caller: one that wants to sharpen a recommendation with check evidence and
|
|
63
|
+
# has a perfectly good answer when there is none. Loads the configuration itself and converts every way the run
|
|
64
|
+
# can fail outright — an unreadable / invalid config, an unloadable plugin, an engine error — into an {Outcome}
|
|
65
|
+
# carrying a short human-readable `error`. It never raises and never fabricates a clean result.
|
|
66
|
+
#
|
|
67
|
+
# Deliberately broad: the caller's contract is "degrade to the presence-only answer, whatever went wrong". A
|
|
68
|
+
# routing hint is outside the false-positive envelope, but a crash in it is not — `describe` must stay a command
|
|
69
|
+
# an agent can run freely.
|
|
70
|
+
#
|
|
71
|
+
# @param config_path [String, nil] path to the config file; nil uses {Configuration.discover}.
|
|
72
|
+
# @param options [Hash] runner options (defaults to {DEEP_OPTIONS}).
|
|
73
|
+
# @param paths [Array<String>, nil]
|
|
74
|
+
# @return [Outcome]
|
|
75
|
+
def attempt(config_path: nil, options: DEEP_OPTIONS, paths: nil)
|
|
76
|
+
require_relative "../configuration"
|
|
77
|
+
configuration = Configuration.load(config_path)
|
|
78
|
+
Outcome.new(result: run(configuration: configuration, options: options, paths: paths).result, error: nil)
|
|
79
|
+
rescue StandardError, LoadError => e
|
|
80
|
+
Outcome.new(result: nil, error: "#{e.class}: #{e.message.to_s.lines.first.to_s.strip}")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -14,6 +14,7 @@ require_relative "../inference/parameter_inference_collector"
|
|
|
14
14
|
require_relative "../protection/mutation_scanner"
|
|
15
15
|
require_relative "../protection/test_suite_oracle"
|
|
16
16
|
require_relative "../language_server/project_context"
|
|
17
|
+
require_relative "../runtime/jit"
|
|
17
18
|
require_relative "../scope"
|
|
18
19
|
require_relative "coverage_report"
|
|
19
20
|
require_relative "coverage_renderer"
|
|
@@ -53,7 +54,11 @@ module Rigor
|
|
|
53
54
|
DEFAULT_TEST_COMMAND = %w[bundle exec rake].freeze
|
|
54
55
|
|
|
55
56
|
# @return [Integer] CLI exit status.
|
|
56
|
-
def run
|
|
57
|
+
def run # rubocop:disable Metrics/AbcSize
|
|
58
|
+
# Arm deferred YJIT enablement (Runtime::Jit): a scan long enough to
|
|
59
|
+
# amortize JIT compile cost enables mid-flight; a short one finishes
|
|
60
|
+
# first and never pays it. Same seam as `rigor check`.
|
|
61
|
+
Runtime::Jit.enable_after(Runtime::Jit.deadline_seconds)
|
|
57
62
|
options = parse_options
|
|
58
63
|
return mutation_misuse_error if options[:mutation] && !options[:protection]
|
|
59
64
|
return with_tests_misuse_error if options[:with_tests] && !options[:mutation]
|
|
@@ -20,9 +20,6 @@ module Rigor
|
|
|
20
20
|
#
|
|
21
21
|
# `<name>` resolves a category-qualified path (`handbook/03-narrowing`), a prefixed basename (`03-narrowing`), or a
|
|
22
22
|
# short name (`narrowing`, when it is unique across categories).
|
|
23
|
-
#
|
|
24
|
-
# The pre-v0.3.0 verb spellings `rigor docs list` / `rigor docs path <name>` still work but emit a stderr
|
|
25
|
-
# deprecation notice; they are removed in v0.3.0 (see docs/ROADMAP.md § "Scheduled CLI deprecations").
|
|
26
23
|
class DocsCommand < Command
|
|
27
24
|
USAGE = <<~USAGE
|
|
28
25
|
Usage: rigor docs [<name>] [--path <name>] [--list [<category>]]
|
|
@@ -46,10 +43,6 @@ module Rigor
|
|
|
46
43
|
rigor docs editor-integration
|
|
47
44
|
rigor docs --path 17-driving-improvement
|
|
48
45
|
rigor docs --list handbook
|
|
49
|
-
|
|
50
|
-
Deprecated (removed in v0.3.0) — use the flags above:
|
|
51
|
-
rigor docs list -> rigor docs --list
|
|
52
|
-
rigor docs path <name> -> rigor docs --path <name>
|
|
53
46
|
USAGE
|
|
54
47
|
|
|
55
48
|
# The bundled docs live at `<gem_root>/docs/`. From `lib/rigor/cli/docs_command.rb` that is three directories up.
|
|
@@ -58,18 +51,8 @@ module Rigor
|
|
|
58
51
|
HANDBOOK_ROOT = File.join(DOCS_ROOT, "handbook")
|
|
59
52
|
LLMS_INDEX = File.join(DOCS_ROOT, "llms.txt")
|
|
60
53
|
|
|
61
|
-
# The verb subcommands the flags superseded keep working with a stderr deprecation notice until this version drops
|
|
62
|
-
# them. Each maps to the canonical advice printed and the flag it rewrites to.
|
|
63
|
-
LEGACY_VERB_REMOVAL = "v0.3.0"
|
|
64
|
-
LEGACY_VERBS = {
|
|
65
|
-
"list" => { old: "list", advice: "--list", flag: "--list" },
|
|
66
|
-
"path" => { old: "path <name>", advice: "--path <name>", flag: "--path" }
|
|
67
|
-
}.freeze
|
|
68
|
-
|
|
69
54
|
# @return [Integer] CLI exit status.
|
|
70
55
|
def run
|
|
71
|
-
rewrite_legacy_verb!
|
|
72
|
-
|
|
73
56
|
case @argv.first
|
|
74
57
|
when nil
|
|
75
58
|
run_index
|
|
@@ -92,19 +75,6 @@ module Rigor
|
|
|
92
75
|
|
|
93
76
|
private
|
|
94
77
|
|
|
95
|
-
# Translate a deprecated verb spelling into its flag form, warning once on stderr, so the dispatch above only
|
|
96
|
-
# handles canonical forms.
|
|
97
|
-
def rewrite_legacy_verb!
|
|
98
|
-
spec = LEGACY_VERBS[@argv.first]
|
|
99
|
-
return unless spec
|
|
100
|
-
|
|
101
|
-
@err.puts(
|
|
102
|
-
"rigor docs: `#{spec.fetch(:old)}` is deprecated and will be removed in " \
|
|
103
|
-
"#{LEGACY_VERB_REMOVAL}; use `rigor docs #{spec.fetch(:advice)}` instead."
|
|
104
|
-
)
|
|
105
|
-
@argv[0] = spec.fetch(:flag)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
78
|
def run_index
|
|
109
79
|
if File.file?(LLMS_INDEX)
|
|
110
80
|
@out.write(File.read(LLMS_INDEX))
|