rigortype 0.2.7 → 0.2.9
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 +1 -1
- data/data/core_overlay/csv.rbs +28 -0
- data/data/core_overlay/psych.rbs +22 -0
- data/docs/handbook/01-getting-started.md +9 -1
- data/docs/handbook/02-everyday-types.md +4 -1
- data/docs/handbook/08-understanding-errors.md +3 -3
- data/docs/manual/01-installation.md +1 -0
- data/docs/manual/02-cli-reference.md +16 -7
- data/docs/manual/04-diagnostics.md +7 -4
- data/docs/manual/06-baseline.md +5 -2
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/14-rails-quickstart.md +4 -2
- data/docs/manual/15-type-protection-coverage.md +27 -4
- data/docs/manual/plugins/rigor-actionpack.md +1 -1
- data/docs/manual/plugins/rigor-activerecord.md +12 -5
- data/docs/manual/plugins/rigor-rails-routes.md +11 -0
- data/lib/rigor/analysis/baseline.rb +56 -96
- data/lib/rigor/analysis/buffer_binding.rb +13 -20
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +29 -48
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +35 -54
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +23 -37
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +16 -23
- data/lib/rigor/analysis/check_rules/rule_walk.rb +48 -71
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +19 -28
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +48 -67
- data/lib/rigor/analysis/check_rules.rb +174 -274
- data/lib/rigor/analysis/dependency_recorder.rb +26 -38
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +13 -25
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +17 -30
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +15 -26
- data/lib/rigor/analysis/dependency_source_inference/index.rb +45 -76
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +23 -40
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +50 -83
- data/lib/rigor/analysis/dependency_source_inference.rb +6 -10
- data/lib/rigor/analysis/diagnostic.rb +48 -75
- data/lib/rigor/analysis/erb_template_detector.rb +13 -18
- data/lib/rigor/analysis/fact_store.rb +12 -22
- data/lib/rigor/analysis/incremental.rb +47 -63
- data/lib/rigor/analysis/incremental_session.rb +74 -100
- data/lib/rigor/analysis/project_scan.rb +12 -23
- data/lib/rigor/analysis/result.rb +3 -5
- data/lib/rigor/analysis/rule_catalog.rb +67 -95
- data/lib/rigor/analysis/run_stats.rb +38 -61
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +101 -160
- data/lib/rigor/analysis/runner/pool_coordinator.rb +121 -188
- data/lib/rigor/analysis/runner/project_pre_passes.rb +62 -96
- data/lib/rigor/analysis/runner/run_snapshots.rb +12 -18
- data/lib/rigor/analysis/runner.rb +220 -333
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +30 -43
- data/lib/rigor/analysis/worker_session.rb +64 -102
- data/lib/rigor/ast/type_node.rb +6 -8
- data/lib/rigor/ast.rb +6 -10
- data/lib/rigor/bleeding_edge.rb +27 -35
- data/lib/rigor/builtins/hkt_builtins.rb +7 -14
- data/lib/rigor/builtins/imported_refinements.rb +106 -149
- data/lib/rigor/builtins/predefined_constant_refinements.rb +51 -65
- data/lib/rigor/builtins/regex_refinement.rb +32 -43
- data/lib/rigor/builtins/static_return_refinements.rb +57 -81
- data/lib/rigor/cache/descriptor.rb +58 -94
- data/lib/rigor/cache/incremental_snapshot.rb +32 -48
- data/lib/rigor/cache/rbs_cache_producer.rb +10 -15
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +10 -16
- data/lib/rigor/cache/rbs_class_type_param_names.rb +10 -16
- data/lib/rigor/cache/rbs_constant_table.rb +8 -13
- data/lib/rigor/cache/rbs_descriptor.rb +8 -13
- data/lib/rigor/cache/rbs_environment.rb +11 -19
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +11 -19
- data/lib/rigor/cache/rbs_known_class_names.rb +8 -12
- data/lib/rigor/cache/store.rb +248 -186
- data/lib/rigor/cli/annotate_command.rb +51 -88
- data/lib/rigor/cli/baseline_command.rb +10 -17
- data/lib/rigor/cli/check_command.rb +121 -199
- data/lib/rigor/cli/check_runner_factory.rb +8 -14
- data/lib/rigor/cli/ci_detector.rb +9 -14
- data/lib/rigor/cli/command.rb +8 -14
- data/lib/rigor/cli/coverage_command.rb +62 -48
- data/lib/rigor/cli/coverage_mutation.rb +14 -19
- data/lib/rigor/cli/coverage_report.rb +0 -2
- data/lib/rigor/cli/coverage_scan.rb +7 -11
- data/lib/rigor/cli/diagnostic_formats.rb +46 -62
- data/lib/rigor/cli/diff_command.rb +10 -20
- data/lib/rigor/cli/docs_command.rb +26 -40
- data/lib/rigor/cli/doctor_command.rb +8 -9
- data/lib/rigor/cli/explain_command.rb +4 -7
- data/lib/rigor/cli/fused_protection_renderer.rb +4 -5
- data/lib/rigor/cli/fused_protection_report.rb +6 -8
- data/lib/rigor/cli/lsp_command.rb +11 -19
- data/lib/rigor/cli/mcp_command.rb +4 -6
- data/lib/rigor/cli/mutation_protection_renderer.rb +3 -4
- data/lib/rigor/cli/mutation_protection_report.rb +6 -9
- data/lib/rigor/cli/options.rb +11 -19
- data/lib/rigor/cli/plugin_command.rb +18 -30
- data/lib/rigor/cli/plugins_command.rb +29 -51
- data/lib/rigor/cli/plugins_renderer.rb +12 -20
- data/lib/rigor/cli/prism_colorizer.rb +13 -19
- data/lib/rigor/cli/protection_fork_scan.rb +55 -0
- data/lib/rigor/cli/protection_renderer.rb +5 -7
- data/lib/rigor/cli/protection_report.rb +34 -18
- data/lib/rigor/cli/renderable.rb +4 -6
- data/lib/rigor/cli/show_bleedingedge_command.rb +5 -8
- data/lib/rigor/cli/sig_gen_command.rb +14 -26
- data/lib/rigor/cli/skill_command.rb +30 -47
- data/lib/rigor/cli/skill_describe.rb +40 -64
- data/lib/rigor/cli/trace_command.rb +9 -16
- data/lib/rigor/cli/trace_renderer.rb +35 -51
- data/lib/rigor/cli/triage_command.rb +6 -10
- data/lib/rigor/cli/triage_renderer.rb +1 -2
- data/lib/rigor/cli/type_of_command.rb +14 -23
- data/lib/rigor/cli/type_of_renderer.rb +3 -5
- data/lib/rigor/cli/type_scan_command.rb +6 -10
- data/lib/rigor/cli/type_scan_renderer.rb +3 -5
- data/lib/rigor/cli/type_scan_report.rb +2 -3
- data/lib/rigor/cli/upgrade_command.rb +2 -3
- data/lib/rigor/cli.rb +16 -28
- data/lib/rigor/config_audit.rb +35 -44
- data/lib/rigor/configuration/dependencies.rb +41 -70
- data/lib/rigor/configuration/severity_profile.rb +28 -42
- data/lib/rigor/configuration.rb +150 -241
- data/lib/rigor/environment/bundle_sig_discovery.rb +74 -118
- data/lib/rigor/environment/class_registry.rb +19 -26
- data/lib/rigor/environment/constant_type_cache_holder.rb +14 -21
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -12
- data/lib/rigor/environment/lockfile_resolver.rb +31 -47
- data/lib/rigor/environment/missing_gem_constant_index.rb +128 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +39 -63
- data/lib/rigor/environment/rbs_coverage_report.rb +24 -42
- data/lib/rigor/environment/rbs_loader.rb +420 -518
- data/lib/rigor/environment/reflection.rb +28 -53
- data/lib/rigor/environment/reporters.rb +13 -24
- data/lib/rigor/environment.rb +225 -304
- data/lib/rigor/flow_contribution/conflict.rb +16 -27
- data/lib/rigor/flow_contribution/element.rb +7 -12
- data/lib/rigor/flow_contribution/fact.rb +33 -57
- data/lib/rigor/flow_contribution/merge_result.rb +6 -9
- data/lib/rigor/flow_contribution/merger.rb +32 -47
- data/lib/rigor/flow_contribution.rb +37 -55
- data/lib/rigor/inference/acceptance.rb +133 -219
- data/lib/rigor/inference/block_parameter_binder.rb +52 -82
- data/lib/rigor/inference/body_fixpoint.rb +22 -30
- data/lib/rigor/inference/budget_trace.rb +43 -63
- data/lib/rigor/inference/builtins/array_catalog.rb +4 -6
- data/lib/rigor/inference/builtins/comparable_catalog.rb +4 -6
- data/lib/rigor/inference/builtins/complex_catalog.rb +13 -19
- data/lib/rigor/inference/builtins/date_catalog.rb +38 -67
- data/lib/rigor/inference/builtins/encoding_catalog.rb +22 -36
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +4 -6
- data/lib/rigor/inference/builtins/exception_catalog.rb +38 -57
- data/lib/rigor/inference/builtins/hash_catalog.rb +9 -14
- data/lib/rigor/inference/builtins/method_catalog.rb +29 -48
- data/lib/rigor/inference/builtins/numeric_catalog.rb +13 -18
- data/lib/rigor/inference/builtins/pathname_catalog.rb +8 -13
- data/lib/rigor/inference/builtins/proc_catalog.rb +38 -62
- data/lib/rigor/inference/builtins/random_catalog.rb +21 -32
- data/lib/rigor/inference/builtins/range_catalog.rb +12 -21
- data/lib/rigor/inference/builtins/rational_catalog.rb +9 -15
- data/lib/rigor/inference/builtins/re_catalog.rb +29 -48
- data/lib/rigor/inference/builtins/set_catalog.rb +18 -27
- data/lib/rigor/inference/builtins/string_catalog.rb +12 -19
- data/lib/rigor/inference/builtins/struct_catalog.rb +18 -30
- data/lib/rigor/inference/builtins/time_catalog.rb +28 -44
- data/lib/rigor/inference/closure_escape_analyzer.rb +44 -67
- data/lib/rigor/inference/coverage_scanner.rb +13 -18
- data/lib/rigor/inference/def_return_typer.rb +8 -14
- data/lib/rigor/inference/dynamic_origin.rb +23 -20
- data/lib/rigor/inference/expression_typer.rb +794 -1154
- data/lib/rigor/inference/fallback.rb +8 -12
- data/lib/rigor/inference/fallback_tracer.rb +4 -10
- data/lib/rigor/inference/flow_tracer.rb +25 -36
- data/lib/rigor/inference/fork_map.rb +87 -0
- data/lib/rigor/inference/hkt_body.rb +45 -68
- data/lib/rigor/inference/hkt_body_parser.rb +33 -52
- data/lib/rigor/inference/hkt_reducer.rb +38 -59
- data/lib/rigor/inference/hkt_registry.rb +50 -76
- data/lib/rigor/inference/indexed_narrowing.rb +55 -81
- data/lib/rigor/inference/macro_block_self_type.rb +21 -34
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +11 -18
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +49 -83
- data/lib/rigor/inference/method_dispatcher/call_context.rb +16 -23
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +13 -22
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +307 -454
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +24 -34
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +22 -37
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +52 -81
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +51 -78
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +60 -97
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +23 -34
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +18 -26
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +37 -71
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +114 -182
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +122 -193
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +15 -24
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +64 -93
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +31 -45
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -9
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +356 -511
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +22 -32
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +15 -21
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +66 -82
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +10 -15
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -13
- data/lib/rigor/inference/method_dispatcher.rb +378 -524
- data/lib/rigor/inference/method_parameter_binder.rb +81 -114
- data/lib/rigor/inference/multi_target_binder.rb +51 -68
- data/lib/rigor/inference/mutation_widening.rb +115 -158
- data/lib/rigor/inference/narrowing.rb +627 -727
- data/lib/rigor/inference/origin_lookup.rb +38 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +154 -109
- data/lib/rigor/inference/precision_scanner.rb +26 -35
- data/lib/rigor/inference/project_patched_methods.rb +20 -32
- data/lib/rigor/inference/project_patched_scanner.rb +23 -37
- data/lib/rigor/inference/protection_scanner.rb +16 -18
- data/lib/rigor/inference/rbs_type_translator.rb +49 -66
- data/lib/rigor/inference/scope_indexer.rb +473 -821
- data/lib/rigor/inference/statement_evaluator.rb +754 -1114
- data/lib/rigor/inference/struct_fold_safety.rb +34 -48
- data/lib/rigor/inference/synthetic_method.rb +9 -16
- data/lib/rigor/inference/synthetic_method_index.rb +20 -35
- data/lib/rigor/inference/synthetic_method_scanner.rb +63 -103
- data/lib/rigor/language_server/buffer_resolution.rb +6 -9
- data/lib/rigor/language_server/buffer_table.rb +11 -18
- data/lib/rigor/language_server/completion_provider.rb +69 -116
- data/lib/rigor/language_server/debouncer.rb +16 -25
- data/lib/rigor/language_server/diagnostic_publisher.rb +26 -40
- data/lib/rigor/language_server/document_symbol_provider.rb +14 -19
- data/lib/rigor/language_server/folding_range_provider.rb +11 -19
- data/lib/rigor/language_server/hover_provider.rb +8 -11
- data/lib/rigor/language_server/hover_renderer.rb +56 -93
- data/lib/rigor/language_server/loop.rb +14 -19
- data/lib/rigor/language_server/project_context.rb +46 -74
- data/lib/rigor/language_server/selection_range_provider.rb +9 -12
- data/lib/rigor/language_server/server.rb +55 -83
- data/lib/rigor/language_server/signature_help_provider.rb +29 -46
- data/lib/rigor/language_server/synchronized_writer.rb +4 -7
- data/lib/rigor/language_server/uri.rb +8 -13
- data/lib/rigor/language_server.rb +4 -6
- data/lib/rigor/mcp/loop.rb +2 -3
- data/lib/rigor/mcp/server.rb +4 -7
- data/lib/rigor/mcp.rb +3 -6
- data/lib/rigor/plugin/access_denied_error.rb +5 -8
- data/lib/rigor/plugin/additional_initializer.rb +21 -31
- data/lib/rigor/plugin/base.rb +335 -518
- data/lib/rigor/plugin/blueprint.rb +14 -23
- data/lib/rigor/plugin/box.rb +18 -29
- data/lib/rigor/plugin/fact_store.rb +16 -26
- data/lib/rigor/plugin/inflector.rb +37 -53
- data/lib/rigor/plugin/io_boundary.rb +33 -56
- data/lib/rigor/plugin/isolation.rb +42 -55
- data/lib/rigor/plugin/load_error.rb +10 -15
- data/lib/rigor/plugin/loader.rb +30 -49
- data/lib/rigor/plugin/macro/block_as_method.rb +20 -32
- data/lib/rigor/plugin/macro/heredoc_template.rb +35 -58
- data/lib/rigor/plugin/macro/nested_class_template.rb +22 -36
- data/lib/rigor/plugin/macro/trait_registry.rb +34 -51
- data/lib/rigor/plugin/macro.rb +10 -15
- data/lib/rigor/plugin/manifest.rb +85 -144
- data/lib/rigor/plugin/node_context.rb +14 -22
- data/lib/rigor/plugin/node_rule_walk.rb +49 -74
- data/lib/rigor/plugin/protocol_contract.rb +25 -39
- data/lib/rigor/plugin/registry.rb +132 -205
- data/lib/rigor/plugin/services.rb +21 -33
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -19
- data/lib/rigor/plugin/trust_policy.rb +24 -38
- data/lib/rigor/plugin/type_node_resolver.rb +15 -24
- data/lib/rigor/protection/diagnostic_oracle.rb +11 -13
- data/lib/rigor/protection/mutation_scanner.rb +27 -35
- data/lib/rigor/protection/mutator.rb +50 -70
- data/lib/rigor/protection/test_suite_oracle.rb +20 -27
- data/lib/rigor/rbs_extended/conformance_checker.rb +48 -68
- data/lib/rigor/rbs_extended/hkt_directives.rb +28 -54
- data/lib/rigor/rbs_extended/reporter.rb +24 -40
- data/lib/rigor/rbs_extended.rb +107 -197
- data/lib/rigor/reflection.rb +68 -86
- data/lib/rigor/scope/discovery_index.rb +14 -19
- data/lib/rigor/scope.rb +255 -310
- data/lib/rigor/sig_gen/classification.rb +6 -10
- data/lib/rigor/sig_gen/generator.rb +197 -323
- data/lib/rigor/sig_gen/layout_index.rb +12 -20
- data/lib/rigor/sig_gen/method_candidate.rb +12 -17
- data/lib/rigor/sig_gen/observation_collector.rb +38 -70
- data/lib/rigor/sig_gen/observed_call.rb +13 -23
- data/lib/rigor/sig_gen/path_mapper.rb +17 -29
- data/lib/rigor/sig_gen/renderer.rb +7 -13
- data/lib/rigor/sig_gen/type_elaborator.rb +15 -28
- data/lib/rigor/sig_gen/write_result.rb +8 -16
- data/lib/rigor/sig_gen/writer.rb +95 -174
- data/lib/rigor/sig_gen.rb +3 -6
- data/lib/rigor/signature_path_audit.rb +24 -30
- data/lib/rigor/source/constant_path.rb +10 -14
- data/lib/rigor/source/literals.rb +31 -45
- data/lib/rigor/source/node_locator.rb +9 -11
- data/lib/rigor/source/node_walker.rb +9 -13
- data/lib/rigor/source.rb +3 -4
- data/lib/rigor/testing.rb +16 -20
- data/lib/rigor/triage/catalogue.rb +38 -62
- data/lib/rigor/triage.rb +31 -52
- data/lib/rigor/trinary.rb +9 -13
- data/lib/rigor/type/acceptance_router.rb +4 -6
- data/lib/rigor/type/accepts_result.rb +10 -14
- data/lib/rigor/type/app.rb +19 -27
- data/lib/rigor/type/bot.rb +4 -6
- data/lib/rigor/type/bound_method.rb +10 -15
- data/lib/rigor/type/combinator.rb +165 -257
- data/lib/rigor/type/constant.rb +23 -34
- data/lib/rigor/type/data_class.rb +10 -15
- data/lib/rigor/type/data_instance.rb +14 -20
- data/lib/rigor/type/difference.rb +21 -32
- data/lib/rigor/type/dynamic.rb +3 -5
- data/lib/rigor/type/hash_shape.rb +32 -18
- data/lib/rigor/type/integer_range.rb +11 -16
- data/lib/rigor/type/intersection.rb +27 -42
- data/lib/rigor/type/nominal.rb +10 -15
- data/lib/rigor/type/plain_lattice.rb +9 -13
- data/lib/rigor/type/refined.rb +67 -114
- data/lib/rigor/type/singleton.rb +4 -6
- data/lib/rigor/type/struct_class.rb +11 -16
- data/lib/rigor/type/struct_instance.rb +15 -21
- data/lib/rigor/type/tuple.rb +14 -19
- data/lib/rigor/type/union.rb +30 -42
- data/lib/rigor/type_node/generic.rb +14 -26
- data/lib/rigor/type_node/identifier.rb +12 -19
- data/lib/rigor/type_node.rb +3 -12
- data/lib/rigor/value_semantics.rb +16 -21
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +18 -30
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +16 -29
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +12 -23
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +22 -39
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +26 -46
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +51 -87
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +10 -17
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +26 -39
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +141 -184
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +30 -51
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +57 -92
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +92 -108
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +13 -22
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +15 -25
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +8 -13
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +16 -24
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +40 -48
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +140 -129
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +56 -59
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +27 -41
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +16 -24
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +172 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +168 -241
- data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +3 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +8 -14
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +9 -15
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +4 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +23 -41
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +3 -5
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +6 -10
- data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +8 -11
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +32 -0
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +30 -45
- data/plugins/rigor-devise/lib/rigor-devise.rb +3 -5
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +43 -69
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +26 -35
- data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +3 -5
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +30 -47
- data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +3 -5
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +67 -113
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +29 -46
- data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +3 -5
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -20
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +14 -23
- data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +2 -3
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +22 -37
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +28 -46
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +13 -17
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -47
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +52 -80
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +21 -31
- data/plugins/rigor-graphql/lib/rigor-graphql.rb +3 -5
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +11 -17
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +20 -34
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +45 -75
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +22 -36
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +22 -41
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +17 -32
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +6 -12
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +4 -8
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +17 -32
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -18
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +58 -92
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +20 -34
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +20 -31
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +49 -82
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +55 -92
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +51 -90
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +17 -30
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +189 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +38 -70
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +61 -75
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +319 -505
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +91 -96
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +32 -54
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +8 -13
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +9 -16
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +24 -39
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +13 -22
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +34 -56
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +22 -42
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +43 -75
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +27 -39
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +14 -19
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +20 -34
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +28 -47
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +21 -37
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +13 -22
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +18 -31
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +6 -10
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +14 -29
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +23 -41
- data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +3 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +23 -38
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +57 -96
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +22 -35
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +41 -69
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +6 -10
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +24 -41
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +29 -50
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +55 -88
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +159 -262
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +21 -29
- data/plugins/rigor-statesman/lib/rigor-statesman.rb +3 -5
- data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +23 -37
- data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +4 -6
- data/sig/rigor/environment.rbs +1 -0
- data/sig/rigor/scope.rbs +6 -0
- metadata +9 -1
data/lib/rigor/cache/store.rb
CHANGED
|
@@ -7,98 +7,99 @@ require "monitor"
|
|
|
7
7
|
require "securerandom"
|
|
8
8
|
require "zlib"
|
|
9
9
|
|
|
10
|
+
require_relative "../version"
|
|
10
11
|
require_relative "descriptor"
|
|
11
12
|
|
|
12
13
|
module Rigor
|
|
13
14
|
module Cache
|
|
14
|
-
# Filesystem-backed cache store. Schema, layout, file format,
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# `#
|
|
18
|
-
# caches, ADR-45), `#stats`, `#evict!`, and `.disk_inventory`,
|
|
19
|
-
# plus the [`Rigor::Cache::Descriptor`](descriptor.rb) value object.
|
|
15
|
+
# Filesystem-backed cache store. Schema, layout, file format, atomicity, and locking are fixed by
|
|
16
|
+
# [ADR-6](../../../docs/adr/6-cache-persistence-backend.md); callers use `#fetch_or_compute`
|
|
17
|
+
# (producer-keyed), `#fetch_or_validate` (record-and-validate for discovered-dep caches, ADR-45), `#stats`,
|
|
18
|
+
# `#evict!`, and `.disk_inventory`, plus the [`Rigor::Cache::Descriptor`](descriptor.rb) value object.
|
|
20
19
|
#
|
|
21
|
-
# Read failures (missing file, bad magic, format-version mismatch,
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# catches accidental corruption (partial writes, FS errors); it is
|
|
26
|
-
# **not** a security boundary, per ADR-2's trusted-gem trust model.
|
|
20
|
+
# Read failures (missing file, bad magic, format-version mismatch, corrupt SHA-256 trailer, un-inflatable
|
|
21
|
+
# or unmarshal-able payload) are silently treated as cache misses; the producer block reruns and the next
|
|
22
|
+
# write replaces the bad entry. The trailing SHA-256 catches accidental corruption (partial writes, FS
|
|
23
|
+
# errors); it is **not** a security boundary, per ADR-2's trusted-gem trust model.
|
|
27
24
|
class Store # rubocop:disable Metrics/ClassLength
|
|
28
|
-
# On-disk byte-layout version. Bumped on incompatible format
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# silent misses; the `schema_version.txt` marker additionally
|
|
36
|
-
# carries this version, so the first writable run after a bump
|
|
37
|
-
# clears the root and reclaims the unreadable bytes.
|
|
25
|
+
# On-disk byte-layout version. Bumped on incompatible format changes (independent of
|
|
26
|
+
# {Descriptor::SCHEMA_VERSION}, which covers the descriptor schema rather than the byte layout). v2
|
|
27
|
+
# (ADR-54 WD2): the value payload is zlib-deflated on write and inflated on read — Marshal blobs
|
|
28
|
+
# compress to 13–16 % at an inflate cost an order of magnitude below their `Marshal.load`. v1 entries
|
|
29
|
+
# fail the header check and read as silent misses; the `schema_version.txt` marker additionally carries
|
|
30
|
+
# this version, so the first writable run after a bump clears the root and reclaims the unreadable
|
|
31
|
+
# bytes.
|
|
38
32
|
FORMAT_VERSION = 2
|
|
39
33
|
|
|
40
|
-
#
|
|
41
|
-
#
|
|
34
|
+
# Payload ABI version. Store values are mostly Marshal blobs of Rigor/RBS objects, so a Rigor release
|
|
35
|
+
# upgrade is an ABI boundary even when the byte layout and descriptor schema are unchanged. Folding the
|
|
36
|
+
# gem version into the root marker makes installed-version upgrades rebuild rather than silently reuse a
|
|
37
|
+
# blob whose class layout still happens to unmarshal.
|
|
38
|
+
PAYLOAD_ABI_VERSION = Rigor::VERSION
|
|
39
|
+
|
|
40
|
+
# Whole-project producers are content-keyed, so dependency / signature churn writes a new entry and leaves
|
|
41
|
+
# the old generation unreachable. The global 256 MB cap is intentionally generous and often never fires on
|
|
42
|
+
# one project, so these producers get a small generation cap as a second compaction axis. Per-file / plugin
|
|
43
|
+
# producers are deliberately absent from this table: many current entries under one producer id can be live.
|
|
44
|
+
GENERATION_CAP_BY_PRODUCER = {
|
|
45
|
+
"analysis.run-diagnostics" => 16,
|
|
46
|
+
"rbs.class_ancestor_table" => 2,
|
|
47
|
+
"rbs.class_type_param_names" => 2,
|
|
48
|
+
"rbs.constant_type_table" => 2,
|
|
49
|
+
"rbs.environment" => 2,
|
|
50
|
+
"rbs.known_class_names" => 2
|
|
51
|
+
}.freeze
|
|
52
|
+
|
|
53
|
+
STALE_TEMP_FILE_AGE_SECONDS = 60 * 60
|
|
54
|
+
|
|
55
|
+
# Header literal: 5-byte ASCII magic, 1-byte separator, 1-byte format version.
|
|
42
56
|
HEADER = "RIGOR\x00#{FORMAT_VERSION.chr}".b.freeze
|
|
43
57
|
|
|
44
58
|
VALID_PRODUCER_ID = /\A[a-z][a-z0-9._-]*\z/
|
|
45
59
|
|
|
46
60
|
# @param root [String] cache root directory.
|
|
47
|
-
# @param read_only [Boolean] when true, every disk-side
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
# populated so repeated lookups within the same run stay
|
|
55
|
-
# cheap. Used by editor mode so multiple buffer-mode
|
|
56
|
-
# invocations can read from the same cache concurrently
|
|
57
|
-
# without churning it. See
|
|
58
|
-
# `docs/design/20260516-editor-mode.md` § "Cache behaviour".
|
|
61
|
+
# @param read_only [Boolean] when true, every disk-side side-effect is suppressed: `fetch_or_compute`
|
|
62
|
+
# still reads existing entries (hits, gated on a current `schema_version.txt` marker — see
|
|
63
|
+
# {#ensure_schema_version!}) and still runs the producer block on miss, but it does NOT write the
|
|
64
|
+
# produced value to disk, does NOT update the marker, and does NOT touch the on-disk root directory.
|
|
65
|
+
# The in-process memo is still populated so repeated lookups within the same run stay cheap. Used by
|
|
66
|
+
# editor mode so multiple buffer-mode invocations can read from the same cache concurrently without
|
|
67
|
+
# churning it. See `docs/design/20260516-editor-mode.md` § "Cache behaviour".
|
|
59
68
|
def initialize(root:, read_only: false, max_bytes: nil)
|
|
60
69
|
@root = root.to_s.dup.freeze
|
|
61
70
|
@read_only = read_only
|
|
62
71
|
@max_bytes = max_bytes&.then { |n| Integer(n) }
|
|
63
|
-
|
|
72
|
+
# Tri-state: nil = not yet checked, true/false = the disk tier's availability for this Store's
|
|
73
|
+
# lifetime (see {#ensure_schema_version!}).
|
|
74
|
+
@disk_available = nil
|
|
64
75
|
@hits = 0
|
|
65
76
|
@misses = 0
|
|
66
77
|
@writes = 0
|
|
67
78
|
@by_producer = Hash.new { |h, k| h[k] = { hits: 0, misses: 0, writes: 0 } }
|
|
68
|
-
# Process-level in-memory layer keyed by
|
|
69
|
-
# `(
|
|
70
|
-
# `
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
# spec process, the LSP daemon's repeated re-check
|
|
74
|
-
# path, and any other "many runs, same project" loop.
|
|
75
|
-
# Keys are content-derived (descriptor digests), so
|
|
76
|
-
# cross-fixture contamination is impossible.
|
|
79
|
+
# Process-level in-memory layer keyed by `(producer_id, cache_key)`. Avoids the disk read +
|
|
80
|
+
# `Marshal.load` cost (the dominant share of repeated cache-hit calls per stackprof) when many
|
|
81
|
+
# short-lived `Analysis::Runner` instances share one `Store` — the spec process, the LSP daemon's
|
|
82
|
+
# repeated re-check path, and any other "many runs, same project" loop. Keys are content-derived
|
|
83
|
+
# (descriptor digests), so cross-fixture contamination is impossible.
|
|
77
84
|
@memo = {}
|
|
78
|
-
# `Analysis::Runner` walks files concurrently (file-
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
# re-entrant so producer blocks can recursively
|
|
83
|
-
# consult the Store (e.g. one cache layer building on
|
|
84
|
-
# another) without dead-locking.
|
|
85
|
+
# `Analysis::Runner` walks files concurrently (file-level parallelism); the per-file workers share
|
|
86
|
+
# one Store. The monitor guards `@memo` + the counter hashes against concurrent writes. The Monitor
|
|
87
|
+
# is re-entrant so producer blocks can recursively consult the Store (e.g. one cache layer building
|
|
88
|
+
# on another) without dead-locking.
|
|
85
89
|
@monitor = Monitor.new
|
|
86
90
|
end
|
|
87
91
|
|
|
88
92
|
attr_reader :root
|
|
89
93
|
|
|
90
|
-
# @return [Boolean] whether this Store suppresses disk writes
|
|
91
|
-
#
|
|
92
|
-
# unaffected.
|
|
94
|
+
# @return [Boolean] whether this Store suppresses disk writes (`schema_version.txt`, entry creation).
|
|
95
|
+
# Reads are unaffected.
|
|
93
96
|
def read_only?
|
|
94
97
|
@read_only
|
|
95
98
|
end
|
|
96
99
|
|
|
97
|
-
# Returns a frozen snapshot of this Store's per-run hit / miss /
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
# against this specific instance rather than the on-disk cache
|
|
101
|
-
# state. Disk-level state is reported separately by
|
|
100
|
+
# Returns a frozen snapshot of this Store's per-run hit / miss / write counters. The bookkeeping is
|
|
101
|
+
# in-memory only — every new `Store.new` starts at zero — so the counters reflect activity against this
|
|
102
|
+
# specific instance rather than the on-disk cache state. Disk-level state is reported separately by
|
|
102
103
|
# {.disk_inventory}.
|
|
103
104
|
#
|
|
104
105
|
# @return [Hash] `{ hits:, misses:, writes:, by_producer: { id => { hits:, misses:, writes: } } }`
|
|
@@ -109,28 +110,21 @@ module Rigor
|
|
|
109
110
|
end
|
|
110
111
|
end
|
|
111
112
|
|
|
112
|
-
# Walks the on-disk cache rooted at `root` and reports a
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
# depending on in-process counters (which only reflect the
|
|
116
|
-
# current run).
|
|
113
|
+
# Walks the on-disk cache rooted at `root` and reports a producer-level inventory. Used by
|
|
114
|
+
# `rigor check --cache-stats` to surface cache size and per-producer entry counts without depending on
|
|
115
|
+
# in-process counters (which only reflect the current run).
|
|
117
116
|
#
|
|
118
|
-
# @return [Hash] `{ root:, schema_version:, total_entries:,
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
# marker, `schema_version` is nil and the producer list is
|
|
122
|
-
# empty.
|
|
117
|
+
# @return [Hash] `{ root:, schema_version:, total_entries:, total_bytes:, producers: [{ id:, entries:,
|
|
118
|
+
# bytes: }, ...] }`. When the root does not exist or has no schema-version marker, `schema_version` is
|
|
119
|
+
# nil and the producer list is empty.
|
|
123
120
|
#
|
|
124
|
-
# The `schema_version.txt` marker content. Covers BOTH
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
# below the eviction cap forever. Folding the format version
|
|
130
|
-
# into the marker routes the bump through the established
|
|
131
|
-
# clear-the-root path instead.
|
|
121
|
+
# The `schema_version.txt` marker content. Covers BOTH invalidation axes: the descriptor schema and the
|
|
122
|
+
# on-disk byte layout ({FORMAT_VERSION}, ADR-54). A format bump leaves the old entries permanently
|
|
123
|
+
# unreadable (header mismatch → miss) but, alone, would never reclaim their bytes — they can sit below
|
|
124
|
+
# the eviction cap forever. Folding the format version into the marker routes the bump through the
|
|
125
|
+
# established clear-the-root path instead.
|
|
132
126
|
def self.schema_marker_value
|
|
133
|
-
"#{Descriptor::SCHEMA_VERSION}.#{FORMAT_VERSION}"
|
|
127
|
+
"#{PAYLOAD_ABI_VERSION}.#{Descriptor::SCHEMA_VERSION}.#{FORMAT_VERSION}"
|
|
134
128
|
end
|
|
135
129
|
|
|
136
130
|
def self.disk_inventory(root:)
|
|
@@ -166,35 +160,25 @@ module Rigor
|
|
|
166
160
|
end
|
|
167
161
|
private_class_method :collect_producers
|
|
168
162
|
|
|
169
|
-
# @param producer_id [String] stable cache namespace; only
|
|
170
|
-
#
|
|
171
|
-
# @param
|
|
172
|
-
#
|
|
173
|
-
#
|
|
174
|
-
#
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
#
|
|
178
|
-
#
|
|
179
|
-
#
|
|
180
|
-
#
|
|
181
|
-
# reads with one strategy and writes with another corrupts
|
|
182
|
-
# its own cache slice.
|
|
183
|
-
# @param deserialize [#call, nil] optional callable that turns
|
|
184
|
-
# bytes back into the producer's value. Defaults to
|
|
185
|
-
# `Marshal.load`. Any exception (`StandardError`) raised by
|
|
186
|
-
# the deserialiser is treated as a cache miss — the entry is
|
|
187
|
-
# considered corrupt, the producer block reruns, and the
|
|
188
|
-
# next write overwrites it. This is consistent with the
|
|
189
|
-
# fault-tolerance contract for the default `Marshal.load`
|
|
163
|
+
# @param producer_id [String] stable cache namespace; only `[a-z][a-z0-9._-]*` is accepted.
|
|
164
|
+
# @param params [Hash] producer inputs; mixed into the cache key via {Descriptor#cache_key_for}.
|
|
165
|
+
# @param descriptor [Rigor::Cache::Descriptor] the invalidation descriptor for the value being cached.
|
|
166
|
+
# @param serialize [#call, nil] optional callable that turns the producer's return value into a binary
|
|
167
|
+
# `String`. Defaults to `Marshal.dump(value).b`. Producers whose return values are not
|
|
168
|
+
# `Marshal`-clean (RBS-native objects with `RBS::Location` members, raw `IO`, …) MUST provide a
|
|
169
|
+
# serialiser. The pair `(serialize, deserialize)` MUST round-trip — a producer that reads with one
|
|
170
|
+
# strategy and writes with another corrupts its own cache slice.
|
|
171
|
+
# @param deserialize [#call, nil] optional callable that turns bytes back into the producer's value.
|
|
172
|
+
# Defaults to `Marshal.load`. Any exception (`StandardError`) raised by the deserialiser is treated as
|
|
173
|
+
# a cache miss — the entry is considered corrupt, the producer block reruns, and the next write
|
|
174
|
+
# overwrites it. This is consistent with the fault-tolerance contract for the default `Marshal.load`
|
|
190
175
|
# path.
|
|
191
176
|
# @yieldreturn the value to cache.
|
|
192
|
-
# @return the cached value (loaded from disk on hit; produced by
|
|
193
|
-
# the block on miss).
|
|
177
|
+
# @return the cached value (loaded from disk on hit; produced by the block on miss).
|
|
194
178
|
def fetch_or_compute(producer_id:, params:, descriptor:,
|
|
195
179
|
serialize: nil, deserialize: nil, &block)
|
|
196
180
|
validate_producer_id!(producer_id)
|
|
197
|
-
ensure_schema_version!
|
|
181
|
+
disk = ensure_schema_version!
|
|
198
182
|
|
|
199
183
|
key = descriptor.cache_key_for(producer_id: producer_id, params: params)
|
|
200
184
|
memo_key = [producer_id, key].freeze
|
|
@@ -204,8 +188,8 @@ module Rigor
|
|
|
204
188
|
return memoed
|
|
205
189
|
end
|
|
206
190
|
|
|
207
|
-
path = entry_path(producer_id, key)
|
|
208
|
-
cached = read_entry(path, deserialize: deserialize)
|
|
191
|
+
path = disk ? entry_path(producer_id, key) : nil
|
|
192
|
+
cached = path && read_entry(path, deserialize: deserialize)
|
|
209
193
|
unless cached.nil?
|
|
210
194
|
@monitor.synchronize do
|
|
211
195
|
record(:hits, producer_id)
|
|
@@ -215,35 +199,31 @@ module Rigor
|
|
|
215
199
|
end
|
|
216
200
|
|
|
217
201
|
value = block.call
|
|
218
|
-
|
|
202
|
+
wrote = path && try_write_entry(path, descriptor, value, serialize: serialize)
|
|
219
203
|
@monitor.synchronize do
|
|
220
204
|
record(:misses, producer_id)
|
|
221
|
-
record(:writes, producer_id)
|
|
205
|
+
record(:writes, producer_id) if wrote
|
|
222
206
|
@memo[memo_key] = value
|
|
223
207
|
end
|
|
224
208
|
value
|
|
225
209
|
end
|
|
226
210
|
|
|
227
|
-
# ADR-45 — record-and-validate variant. Unlike {fetch_or_compute},
|
|
228
|
-
#
|
|
229
|
-
# known
|
|
230
|
-
#
|
|
231
|
-
#
|
|
232
|
-
# including inputs discovered DURING the computation (e.g. a plugin
|
|
233
|
-
# reading a file mid-analysis). On the next run the stored
|
|
234
|
-
# dependencies are re-validated against the filesystem
|
|
211
|
+
# ADR-45 — record-and-validate variant. Unlike {fetch_or_compute}, which keys the entry on its
|
|
212
|
+
# descriptor (so the inputs MUST be known before running), this keys on `key_descriptor` (the stable
|
|
213
|
+
# inputs known up front) and stores, alongside the value, a `dependency_descriptor` of the files the
|
|
214
|
+
# value actually read — including inputs discovered DURING the computation (e.g. a plugin reading a
|
|
215
|
+
# file mid-analysis). On the next run the stored dependencies are re-validated against the filesystem
|
|
235
216
|
# ({Descriptor#fresh?}); a stale dependency forces a recompute.
|
|
236
217
|
#
|
|
237
|
-
# The block MUST return `[value, dependency_descriptor]`. Disk reads
|
|
238
|
-
#
|
|
239
|
-
# filesystem — but a single run only looks up once.
|
|
218
|
+
# The block MUST return `[value, dependency_descriptor]`. Disk reads are not in-process-memoised —
|
|
219
|
+
# validation always re-checks the filesystem — but a single run only looks up once.
|
|
240
220
|
def fetch_or_validate(producer_id:, key_descriptor:, params: {}, serialize: nil, deserialize: nil)
|
|
241
221
|
validate_producer_id!(producer_id)
|
|
242
|
-
ensure_schema_version!
|
|
222
|
+
disk = ensure_schema_version!
|
|
243
223
|
|
|
244
224
|
key = key_descriptor.cache_key_for(producer_id: producer_id, params: params)
|
|
245
|
-
path = entry_path(producer_id, key)
|
|
246
|
-
cached = read_entry(path, deserialize: deserialize)
|
|
225
|
+
path = disk ? entry_path(producer_id, key) : nil
|
|
226
|
+
cached = path && read_entry(path, deserialize: deserialize)
|
|
247
227
|
if cached && (pair = cached.value).is_a?(Array) && pair.size == 2 &&
|
|
248
228
|
pair[1].is_a?(Descriptor) && pair[1].fresh?
|
|
249
229
|
@monitor.synchronize { record(:hits, producer_id) }
|
|
@@ -251,18 +231,7 @@ module Rigor
|
|
|
251
231
|
end
|
|
252
232
|
|
|
253
233
|
value, dependency_descriptor = block_given? ? yield : [nil, Descriptor.new]
|
|
254
|
-
wrote =
|
|
255
|
-
unless @read_only
|
|
256
|
-
# A cache write must never break the run. If the value is not
|
|
257
|
-
# Marshal-clean (or any disk error occurs) skip caching and
|
|
258
|
-
# return the freshly-computed value — the next run recomputes.
|
|
259
|
-
begin
|
|
260
|
-
write_entry(path, key_descriptor, [value, dependency_descriptor], serialize: serialize)
|
|
261
|
-
wrote = true
|
|
262
|
-
rescue StandardError
|
|
263
|
-
wrote = false
|
|
264
|
-
end
|
|
265
|
-
end
|
|
234
|
+
wrote = path && try_write_entry(path, key_descriptor, [value, dependency_descriptor], serialize: serialize)
|
|
266
235
|
@monitor.synchronize do
|
|
267
236
|
record(:misses, producer_id)
|
|
268
237
|
record(:writes, producer_id) if wrote
|
|
@@ -270,29 +239,30 @@ module Rigor
|
|
|
270
239
|
value
|
|
271
240
|
end
|
|
272
241
|
|
|
273
|
-
# ADR-6 § "Eviction" —
|
|
274
|
-
#
|
|
275
|
-
#
|
|
276
|
-
#
|
|
277
|
-
#
|
|
278
|
-
#
|
|
279
|
-
#
|
|
280
|
-
#
|
|
242
|
+
# ADR-6 § "Eviction" — compaction pass over the on-disk cache. No-op when the store is read-only. Stale
|
|
243
|
+
# temp file cleanup and the whole-project generation cap run regardless of `max_bytes:` — they reclaim
|
|
244
|
+
# provably-dead bytes (leaked temp files, unreachable content-keyed generations) rather than enforcing a
|
|
245
|
+
# size budget, so an explicitly unbounded store (`max_bytes: nil`) still benefits from them. The
|
|
246
|
+
# size-based LRU pass below stays gated on `max_bytes:` being configured: it walks all remaining
|
|
247
|
+
# `.entry` files, sorts by mtime ascending (oldest = least recently used), and unlinks from the oldest
|
|
248
|
+
# until the total is at or below the cap. Touch-on-disk-read ({read_entry}) is the cross-process LRU
|
|
249
|
+
# signal: every disk hit (not in-process-memo hit) updates the mtime so recently-read entries survive
|
|
250
|
+
# the eviction pass. Any FS error is swallowed — eviction must never break a run.
|
|
281
251
|
def evict!
|
|
282
|
-
return if @
|
|
252
|
+
return if @read_only
|
|
253
|
+
|
|
254
|
+
cleanup_stale_temp_files
|
|
255
|
+
entries = evict_excess_generations(collect_entry_stats)
|
|
256
|
+
return if @max_bytes.nil?
|
|
283
257
|
|
|
284
|
-
|
|
285
|
-
total = entries.sum { |e| e[:bytes] }
|
|
258
|
+
total = entries.sum { |e| e[:bytes] }
|
|
286
259
|
return if total <= @max_bytes
|
|
287
260
|
|
|
288
261
|
entries.sort_by! { |e| e[:mtime] }
|
|
289
262
|
entries.each do |entry|
|
|
290
263
|
break if total <= @max_bytes
|
|
291
264
|
|
|
292
|
-
|
|
293
|
-
total -= entry[:bytes]
|
|
294
|
-
rescue StandardError
|
|
295
|
-
next
|
|
265
|
+
total -= entry[:bytes] if unlink_entry(entry[:path])
|
|
296
266
|
end
|
|
297
267
|
nil
|
|
298
268
|
rescue StandardError
|
|
@@ -324,9 +294,8 @@ module Rigor
|
|
|
324
294
|
File.join(@root, producer_id, key[0, 2], "#{key[2..]}.entry")
|
|
325
295
|
end
|
|
326
296
|
|
|
327
|
-
# Reads and validates one entry file. Any failure (missing,
|
|
328
|
-
#
|
|
329
|
-
# returns nil so the caller treats it as a cache miss.
|
|
297
|
+
# Reads and validates one entry file. Any failure (missing, short, bad magic, bad version, bad
|
|
298
|
+
# checksum, unmarshal-able) returns nil so the caller treats it as a cache miss.
|
|
330
299
|
def read_entry(path, deserialize: nil)
|
|
331
300
|
return nil unless File.file?(path)
|
|
332
301
|
|
|
@@ -344,8 +313,8 @@ module Rigor
|
|
|
344
313
|
Entry.new(descriptor_bytes, value)
|
|
345
314
|
end
|
|
346
315
|
|
|
347
|
-
# Validates the magic + format-version header and the trailing
|
|
348
|
-
#
|
|
316
|
+
# Validates the magic + format-version header and the trailing SHA-256 over everything before the
|
|
317
|
+
# trailer.
|
|
349
318
|
def envelope_valid?(bytes)
|
|
350
319
|
return false if bytes.bytesize < HEADER.bytesize + 32
|
|
351
320
|
return false unless bytes.byteslice(0, HEADER.bytesize) == HEADER
|
|
@@ -354,8 +323,8 @@ module Rigor
|
|
|
354
323
|
Digest::SHA256.digest(bytes.byteslice(0, bytes.bytesize - 32)) == trailer
|
|
355
324
|
end
|
|
356
325
|
|
|
357
|
-
# Splits the body into (descriptor_bytes, value_bytes). Returns
|
|
358
|
-
#
|
|
326
|
+
# Splits the body into (descriptor_bytes, value_bytes). Returns `[nil, nil]` on a malformed varint or
|
|
327
|
+
# length-overrun.
|
|
359
328
|
def parse_body(body)
|
|
360
329
|
offset = 0
|
|
361
330
|
descriptor_len, offset = read_varint(body, offset)
|
|
@@ -374,9 +343,8 @@ module Rigor
|
|
|
374
343
|
LOAD_FAILED = Object.new.freeze
|
|
375
344
|
private_constant :LOAD_FAILED
|
|
376
345
|
|
|
377
|
-
# Inflates the stored value payload (ADR-54 WD2), then hands the
|
|
378
|
-
#
|
|
379
|
-
# stream included — reads as a miss.
|
|
346
|
+
# Inflates the stored value payload (ADR-54 WD2), then hands the raw bytes to the deserialiser. Any
|
|
347
|
+
# failure — corrupt deflate stream included — reads as a miss.
|
|
380
348
|
def safe_load(bytes, deserialize)
|
|
381
349
|
raw = Zlib::Inflate.inflate(bytes)
|
|
382
350
|
if deserialize
|
|
@@ -421,42 +389,68 @@ module Rigor
|
|
|
421
389
|
File.open(path, File::RDWR | File::CREAT, 0o644) do |lock_fd|
|
|
422
390
|
lock_fd.flock(File::LOCK_EX)
|
|
423
391
|
tmp = "#{path}.tmp.#{Process.pid}.#{SecureRandom.hex(4)}"
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
392
|
+
begin
|
|
393
|
+
File.open(tmp, "wb") do |f|
|
|
394
|
+
f.write(body)
|
|
395
|
+
f.fsync
|
|
396
|
+
end
|
|
397
|
+
File.rename(tmp, path)
|
|
398
|
+
ensure
|
|
399
|
+
# A failed write/rename must not leak its temp file — rely on the 1-hour `cleanup_stale_temp_files`
|
|
400
|
+
# sweep only as a backstop for crashes that skip this ensure entirely.
|
|
401
|
+
unlink_entry(tmp) if File.exist?(tmp)
|
|
427
402
|
end
|
|
428
|
-
File.
|
|
403
|
+
fsync_directory(File.dirname(path))
|
|
429
404
|
end
|
|
430
405
|
end
|
|
431
406
|
|
|
407
|
+
# Checks (and, for a writable store, repairs) the `schema_version.txt` marker, and reports whether the
|
|
408
|
+
# disk tier is usable for the rest of this Store's lifetime. The result is memoized in `@disk_available`
|
|
409
|
+
# — one check per Store is enough; a benign double-check under a thread race would just repeat
|
|
410
|
+
# idempotent work.
|
|
411
|
+
#
|
|
412
|
+
# A writable store clears the cache root and rewrites the marker on a stale/missing marker, then reports
|
|
413
|
+
# available. A read-only store never touches the root — no mkdir, no marker write, no destructive clear
|
|
414
|
+
# — so it reports available ONLY when the on-disk marker already matches current exactly; a stale or
|
|
415
|
+
# missing marker (e.g. a Rigor upgrade with no writable run yet, as in LSP / editor mode) reports
|
|
416
|
+
# unavailable rather than risk unmarshalling a payload from a different ABI. Any filesystem failure
|
|
417
|
+
# (permission, disk full, deleted root) also reports unavailable, degrading this Store instance to
|
|
418
|
+
# in-memory-memo-only for the rest of its lifetime — the producer block still runs, its result still
|
|
419
|
+
# lands in `@memo`, and disk reads/writes are simply skipped.
|
|
420
|
+
#
|
|
421
|
+
# @return [Boolean]
|
|
432
422
|
def ensure_schema_version!
|
|
433
|
-
|
|
434
|
-
# no marker write, no destructive clear on schema
|
|
435
|
-
# mismatch. A stale or wrong-schema marker simply yields
|
|
436
|
-
# nothing back (entries read through the version check
|
|
437
|
-
# are content-keyed, so a write under the new schema
|
|
438
|
-
# never collides with a read under the old). The next
|
|
439
|
-
# writable run will repair the cache.
|
|
440
|
-
return if @read_only
|
|
441
|
-
# The marker is process-stable; one check per Store is
|
|
442
|
-
# enough (a benign double-check under a thread race just
|
|
443
|
-
# repeats idempotent work).
|
|
444
|
-
return if @schema_version_ensured
|
|
423
|
+
return @disk_available unless @disk_available.nil?
|
|
445
424
|
|
|
446
|
-
@
|
|
425
|
+
@disk_available = @read_only ? read_only_marker_current? : repair_writable_marker!
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def read_only_marker_current?
|
|
429
|
+
marker = File.join(@root, "schema_version.txt")
|
|
430
|
+
return false unless File.file?(marker)
|
|
431
|
+
|
|
432
|
+
File.read(marker).strip == self.class.schema_marker_value
|
|
433
|
+
rescue StandardError
|
|
434
|
+
false
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def repair_writable_marker!
|
|
447
438
|
FileUtils.mkdir_p(@root)
|
|
448
439
|
marker = File.join(@root, "schema_version.txt")
|
|
449
440
|
current = self.class.schema_marker_value
|
|
450
441
|
|
|
451
442
|
if File.file?(marker)
|
|
452
443
|
on_disk = File.read(marker).strip
|
|
453
|
-
return if on_disk == current
|
|
444
|
+
return true if on_disk == current
|
|
454
445
|
|
|
455
446
|
clear_cache_root!
|
|
456
447
|
end
|
|
457
448
|
|
|
458
449
|
FileUtils.mkdir_p(@root)
|
|
459
450
|
File.write(marker, "#{current}\n")
|
|
451
|
+
true
|
|
452
|
+
rescue StandardError
|
|
453
|
+
false
|
|
460
454
|
end
|
|
461
455
|
|
|
462
456
|
def clear_cache_root!
|
|
@@ -465,9 +459,29 @@ module Rigor
|
|
|
465
459
|
end
|
|
466
460
|
end
|
|
467
461
|
|
|
468
|
-
#
|
|
469
|
-
#
|
|
470
|
-
#
|
|
462
|
+
# Shared write path for both {#fetch_or_compute} and {#fetch_or_validate}. A cache write must never
|
|
463
|
+
# break the run: filesystem-side failures (permission, disk full, deleted root, read-only mount) are
|
|
464
|
+
# swallowed and read as "did not write". Programmer errors — a custom serializer that doesn't return a
|
|
465
|
+
# `String`, or any other producer contract violation — still raise so the bug is visible.
|
|
466
|
+
def try_write_entry(path, descriptor, value, serialize: nil)
|
|
467
|
+
return false if @read_only
|
|
468
|
+
|
|
469
|
+
write_entry(path, descriptor, value, serialize: serialize)
|
|
470
|
+
true
|
|
471
|
+
rescue SystemCallError, IOError
|
|
472
|
+
false
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# Best-effort durability for the rename itself. Some platforms cannot fsync directories (or do not need to),
|
|
476
|
+
# so failures are ignored; the entry envelope still turns any lost/partial write into a miss.
|
|
477
|
+
def fsync_directory(dir)
|
|
478
|
+
File.open(dir, File::RDONLY, &:fsync)
|
|
479
|
+
rescue StandardError
|
|
480
|
+
nil
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# LEB128 unsigned varint encoder/decoder. Lengths fit easily in five bytes (cap at 2^35); the cache
|
|
484
|
+
# layer never writes a value larger than that in practice.
|
|
471
485
|
def write_varint(bytes, value)
|
|
472
486
|
raise ArgumentError, "varint must be non-negative" if value.negative?
|
|
473
487
|
|
|
@@ -482,9 +496,8 @@ module Rigor
|
|
|
482
496
|
end
|
|
483
497
|
end
|
|
484
498
|
|
|
485
|
-
# Updates both atime and mtime of `path` to the current time — the
|
|
486
|
-
#
|
|
487
|
-
# error (read-only mount, deleted file) is silently ignored.
|
|
499
|
+
# Updates both atime and mtime of `path` to the current time — the cross-process LRU signal used by
|
|
500
|
+
# {evict!}. Best-effort: any FS error (read-only mount, deleted file) is silently ignored.
|
|
488
501
|
def touch_for_lru(path)
|
|
489
502
|
now = Time.now
|
|
490
503
|
File.utime(now, now, path)
|
|
@@ -492,17 +505,66 @@ module Rigor
|
|
|
492
505
|
nil
|
|
493
506
|
end
|
|
494
507
|
|
|
495
|
-
|
|
496
|
-
|
|
508
|
+
def cleanup_stale_temp_files
|
|
509
|
+
cutoff = Time.now - STALE_TEMP_FILE_AGE_SECONDS
|
|
510
|
+
Dir.glob(File.join(@root, "**", "*.tmp.*")).each do |path|
|
|
511
|
+
next unless File.file?(path)
|
|
512
|
+
next if File.mtime(path) > cutoff
|
|
513
|
+
|
|
514
|
+
unlink_entry(path)
|
|
515
|
+
rescue StandardError
|
|
516
|
+
next
|
|
517
|
+
end
|
|
518
|
+
rescue StandardError
|
|
519
|
+
nil
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
def evict_excess_generations(entries)
|
|
523
|
+
removed = {}
|
|
524
|
+
entries.group_by { |entry| entry[:producer] }.each do |producer, producer_entries|
|
|
525
|
+
cap = GENERATION_CAP_BY_PRODUCER[producer]
|
|
526
|
+
next if cap.nil? || producer_entries.size <= cap
|
|
527
|
+
|
|
528
|
+
producer_entries.sort_by { |entry| [entry[:mtime], entry[:path]] }
|
|
529
|
+
.first(producer_entries.size - cap)
|
|
530
|
+
.each do |entry|
|
|
531
|
+
removed[entry[:path]] = true if unlink_entry(entry[:path])
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
return entries if removed.empty?
|
|
535
|
+
|
|
536
|
+
entries.reject { |entry| removed[entry[:path]] }
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def unlink_entry(path)
|
|
540
|
+
File.unlink(path)
|
|
541
|
+
true
|
|
542
|
+
rescue StandardError
|
|
543
|
+
false
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# Returns an array of `{ path:, producer:, mtime:, bytes: }` hashes for every `.entry` file under the
|
|
547
|
+
# cache root, skipping unreadable entries.
|
|
497
548
|
def collect_entry_stats
|
|
498
549
|
Dir.glob(File.join(@root, "**", "*.entry")).filter_map do |path|
|
|
499
550
|
stat = File.stat(path)
|
|
500
|
-
|
|
551
|
+
producer = producer_id_for_entry(path)
|
|
552
|
+
next nil if producer.nil?
|
|
553
|
+
|
|
554
|
+
{ path: path, producer: producer, mtime: stat.mtime, bytes: stat.size }
|
|
501
555
|
rescue StandardError
|
|
502
556
|
nil
|
|
503
557
|
end
|
|
504
558
|
end
|
|
505
559
|
|
|
560
|
+
def producer_id_for_entry(path)
|
|
561
|
+
root_prefix = @root.end_with?(File::SEPARATOR) ? @root : "#{@root}#{File::SEPARATOR}"
|
|
562
|
+
return nil unless path.start_with?(root_prefix)
|
|
563
|
+
|
|
564
|
+
producer = path.delete_prefix(root_prefix).split(File::SEPARATOR, 2).first
|
|
565
|
+
producer.empty? ? nil : producer
|
|
566
|
+
end
|
|
567
|
+
|
|
506
568
|
def read_varint(bytes, offset)
|
|
507
569
|
result = 0
|
|
508
570
|
shift = 0
|