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
|
@@ -24,8 +24,22 @@ module Rigor
|
|
|
24
24
|
# Bump when the on-disk shape changes so stale snapshots are ignored rather than mis-deserialized. 5:
|
|
25
25
|
# the blob is zlib-deflated (ADR-54 WD2 parity with `Store` entries — the snapshot is the one cache
|
|
26
26
|
# artefact that does not go through `Store`); a raw pre-5 blob fails the inflate and loads as nil, the
|
|
27
|
-
# usual fault-tolerant cold-run path.
|
|
28
|
-
|
|
27
|
+
# usual fault-tolerant cold-run path. 6: adds the ADR-85 WD2 `seed_bundles` section (per-file discovery
|
|
28
|
+
# contributions with `(node_id, name, fingerprint)` def-node handles); a pre-6 blob mismatches the
|
|
29
|
+
# SCHEMA gate and loads as nil (a clean cold rebuild — no migration). 7: the seed bundle gains a
|
|
30
|
+
# `singleton_def_sources` table (ADR-46 slice 4 extended to class/singleton methods) AND `digests`
|
|
31
|
+
# switches to ADR-87 packed stat entries; a pre-7 blob mismatches the gate and loads as nil (clean cold
|
|
32
|
+
# rebuild). 8: each seed bundle additionally gains a comment-stripped `code_fingerprint` for the B1
|
|
33
|
+
# bundle-equality gate; a pre-8 blob mismatches and loads as nil (clean cold rebuild). 9: adds the ADR-88
|
|
34
|
+
# WD1 `plugin_fact_digest` (a fingerprint of the plugin fact SURFACE — ADR-9 facts, ADR-60 producer
|
|
35
|
+
# values, and `incremental_state_fingerprint` hooks — that a cached diagnostic can depend on but the
|
|
36
|
+
# global fingerprint does not capture); a pre-9 blob mismatches the SCHEMA gate and loads as nil (a clean
|
|
37
|
+
# cold rebuild — no migration). 10: ADR-89 WD1 adds a per-file `declaration_signature` to each seed
|
|
38
|
+
# bundle (the per-def parameter-shape / visibility / ancestry surface the declaration-stability gate
|
|
39
|
+
# compares) and WD2 adds `return_summaries` (per-def observed-key return descriptors + mutation-effect
|
|
40
|
+
# sets the behavioural-stability gate compares); a pre-10 blob mismatches the SCHEMA gate and loads as
|
|
41
|
+
# nil (a clean cold rebuild — no migration).
|
|
42
|
+
SCHEMA = 10
|
|
29
43
|
|
|
30
44
|
# The persisted per-file state.
|
|
31
45
|
# `cache` maps an analyzed file to its diagnostics.
|
|
@@ -39,9 +53,25 @@ module Rigor
|
|
|
39
53
|
# ADR-46 slice 3:
|
|
40
54
|
# `missing` maps a consumer to Set<"kind:name"> it looked up and missed.
|
|
41
55
|
# `class_decls` maps a path to Set<qualified class name> it declares.
|
|
56
|
+
# ADR-85 WD2:
|
|
57
|
+
# `seed_bundles` maps an analyzed path to its per-file discovery contribution (plain-data tables +
|
|
58
|
+
# `(node_id, name, fingerprint)` def-node handles + content digest), so a warm recheck rebuilds the
|
|
59
|
+
# cross-file index by folding bundles instead of parsing every file.
|
|
60
|
+
# ADR-88 WD1:
|
|
61
|
+
# `plugin_fact_digest` is a SHA-256 hex fingerprint of the plugin fact surface at the run that wrote the
|
|
62
|
+
# snapshot (or nil for a plugin-free project). A warm recheck recomputes it and, on a mismatch, discards
|
|
63
|
+
# the snapshot and runs a full analysis — the guard for a plugin sig/catalog edit that the global
|
|
64
|
+
# fingerprint cannot see.
|
|
65
|
+
# ADR-89 WD2:
|
|
66
|
+
# `return_summaries` maps a `[path, "Class#method" | "Class.method"]` to the callee's persisted
|
|
67
|
+
# behavioural surface `{ keys:, returns:, effects: }` — the observed `[receiver, arg_types]` call keys
|
|
68
|
+
# (Marshal-clean type tuples), their `describe(:short)` return descriptors, and the content-mutated
|
|
69
|
+
# parameter positions. A recheck re-evaluates a declaration-stable changed callee at these keys and,
|
|
70
|
+
# when every return + the effects are unchanged, skips its symbol dependents.
|
|
42
71
|
Payload = Data.define(:cache, :sources, :digests, :analyzed,
|
|
43
72
|
:symbol_sources, :ancestry_sources, :symbol_fingerprints,
|
|
44
|
-
:missing, :class_decls
|
|
73
|
+
:missing, :class_decls, :seed_bundles, :plugin_fact_digest,
|
|
74
|
+
:return_summaries)
|
|
45
75
|
|
|
46
76
|
# The global fingerprint that gates a snapshot load: a digest of the inputs whose change requires a full
|
|
47
77
|
# rebuild — the engine version + schema, the resolved configuration, the analysis **roots** (the path
|
|
@@ -104,7 +134,10 @@ module Rigor
|
|
|
104
134
|
ancestry_sources: data[:ancestry_sources] || {},
|
|
105
135
|
symbol_fingerprints: data[:symbol_fingerprints] || {},
|
|
106
136
|
missing: data[:missing] || {},
|
|
107
|
-
class_decls: data[:class_decls] || {}
|
|
137
|
+
class_decls: data[:class_decls] || {},
|
|
138
|
+
seed_bundles: data[:seed_bundles] || {},
|
|
139
|
+
plugin_fact_digest: data[:plugin_fact_digest],
|
|
140
|
+
return_summaries: data[:return_summaries] || {}
|
|
108
141
|
)
|
|
109
142
|
rescue StandardError
|
|
110
143
|
nil
|
|
@@ -122,7 +155,10 @@ module Rigor
|
|
|
122
155
|
ancestry_sources: payload.ancestry_sources,
|
|
123
156
|
symbol_fingerprints: payload.symbol_fingerprints,
|
|
124
157
|
missing: payload.missing,
|
|
125
|
-
class_decls: payload.class_decls
|
|
158
|
+
class_decls: payload.class_decls,
|
|
159
|
+
seed_bundles: payload.seed_bundles,
|
|
160
|
+
plugin_fact_digest: payload.plugin_fact_digest,
|
|
161
|
+
return_summaries: payload.return_summaries
|
|
126
162
|
)
|
|
127
163
|
blob = Zlib::Deflate.deflate(raw)
|
|
128
164
|
tmp = "#{@path}.#{Process.pid}.tmp"
|
|
@@ -15,12 +15,22 @@ module Rigor
|
|
|
15
15
|
# `self::PRODUCER_ID` resolves the constant on the concrete subclass, and `compute(loader)` dispatches to
|
|
16
16
|
# its private class method. See the `_CacheProducer` RBS interface for the structural contract.
|
|
17
17
|
class RbsCacheProducer
|
|
18
|
+
# Every RBS producer is whole-project and content-keyed: one entry is live and any older generation is
|
|
19
|
+
# unreachable, so `Cache::Store#evict!` keeps a small number of them (one spare for the still-warm
|
|
20
|
+
# previous signature state — the RBS environment blob alone runs to ~1.8 MB, ADR-54 WD3). Declared on
|
|
21
|
+
# the base, so a producer added by subclassing inherits a cap instead of being silently uncapped; a
|
|
22
|
+
# subclass with different economics overrides this method.
|
|
23
|
+
def self.generation_cap
|
|
24
|
+
2
|
|
25
|
+
end
|
|
26
|
+
|
|
18
27
|
def self.fetch(loader:, store:)
|
|
19
28
|
# ADR-54 WD4 — the descriptor is identical for every producer consulting the same loader (same sig
|
|
20
29
|
# files, same libraries), so the loader memoises one build per process instead of re-digesting every
|
|
21
30
|
# .rbs file once per producer.
|
|
22
31
|
descriptor = loader.rbs_cache_descriptor
|
|
23
|
-
store.fetch_or_compute(producer_id: self::PRODUCER_ID, params: {}, descriptor: descriptor
|
|
32
|
+
store.fetch_or_compute(producer_id: self::PRODUCER_ID, params: {}, descriptor: descriptor,
|
|
33
|
+
generation_cap: generation_cap) do
|
|
24
34
|
compute(loader)
|
|
25
35
|
end
|
|
26
36
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "digest"
|
|
4
4
|
|
|
5
5
|
require_relative "descriptor"
|
|
6
|
+
require_relative "file_digest"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module Cache
|
|
@@ -17,15 +18,36 @@ module Rigor
|
|
|
17
18
|
Descriptor.new(
|
|
18
19
|
gems: [rbs_gem_entry],
|
|
19
20
|
files: file_entries(loader),
|
|
20
|
-
configs:
|
|
21
|
+
configs: config_entries(loader)
|
|
21
22
|
)
|
|
22
23
|
end
|
|
23
24
|
|
|
25
|
+
# Lazy-files variant for the ADR-45 run-diagnostics record-and-validate cache. The cache KEY reads only
|
|
26
|
+
# `gems` + `configs` ({Runner#run_key_descriptor}); the RBS signature-tree `files` are read solely on a
|
|
27
|
+
# MISS, by the dependency descriptor ({Runner#run_dependency_descriptor}). So a warm HIT never digests
|
|
28
|
+
# the (large, vendored) RBS tree. {RunDescriptor} is NOT a {Descriptor} — it is never composed, hashed,
|
|
29
|
+
# or `==`'d, only its three readers are consulted — so deferring `files` costs no soundness, and `gems`
|
|
30
|
+
# / `configs` are byte-identical to {.build} (the key is unchanged).
|
|
31
|
+
def self.build_run(loader)
|
|
32
|
+
RunDescriptor.new(loader: loader, gems: [rbs_gem_entry], configs: config_entries(loader))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The `gems` + `configs` slots the run cache key reads. Cheap — no RBS env load, no file digesting
|
|
36
|
+
# (only the configured library names + any plugin-synthesised virtual RBS are hashed).
|
|
37
|
+
def self.config_entries(loader)
|
|
38
|
+
[libraries_entry(loader.libraries), virtual_rbs_entry(loader)].compact
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Public (ADR-87 WD4) so the boot-slimming run-cache probe reconstructs the identical `gems` +
|
|
42
|
+
# `rbs.libraries` key slots the runner writes, from the config-derived library list, without a loader.
|
|
24
43
|
def self.rbs_gem_entry
|
|
25
44
|
Descriptor::GemEntry.new(name: "rbs", requirement: ">= 0", locked: ::RBS::VERSION.to_s)
|
|
26
45
|
end
|
|
27
46
|
|
|
28
|
-
|
|
47
|
+
# @param comparator [Symbol] `:digest` (default) for the env-cache KEY descriptor ({.build}), where the
|
|
48
|
+
# value must be deterministic; `:stat` (ADR-87 WD1) for the validation-only run-dependency descriptor
|
|
49
|
+
# ({RunDescriptor#files}), where the stat tier short-circuits the SHA-256 on an unmoved file.
|
|
50
|
+
def self.file_entries(loader, comparator: :digest)
|
|
29
51
|
roots = loader.signature_paths +
|
|
30
52
|
Rigor::Environment::RbsLoader.vendored_gem_sig_paths +
|
|
31
53
|
Rigor::Environment::RbsLoader.core_overlay_sig_paths
|
|
@@ -33,17 +55,20 @@ module Rigor
|
|
|
33
55
|
next [] unless root.directory?
|
|
34
56
|
|
|
35
57
|
Dir.glob(root.join("**", "*.rbs")).map do |path|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
58
|
+
digest = FileDigest.hexdigest(path)
|
|
59
|
+
if comparator == :stat
|
|
60
|
+
Descriptor::FileEntry.stat(path: path, digest: digest)
|
|
61
|
+
else
|
|
62
|
+
Descriptor::FileEntry.new(path: path, comparator: :digest, value: digest)
|
|
63
|
+
end
|
|
41
64
|
end
|
|
42
65
|
end
|
|
43
66
|
end
|
|
44
67
|
|
|
45
|
-
|
|
46
|
-
|
|
68
|
+
# @param library_names [Array<String, Symbol>] the loader's merged library list (or, on the WD4 probe
|
|
69
|
+
# path, `Environment::DEFAULT_LIBRARIES + config.libraries` reconstructed without a loader).
|
|
70
|
+
def self.libraries_entry(library_names)
|
|
71
|
+
sorted = library_names.map(&:to_s).sort
|
|
47
72
|
Descriptor::ConfigEntry.new(
|
|
48
73
|
key: "rbs.libraries",
|
|
49
74
|
value_hash: Digest::SHA256.hexdigest(sorted.join("\n"))
|
|
@@ -66,7 +91,28 @@ module Rigor
|
|
|
66
91
|
)
|
|
67
92
|
end
|
|
68
93
|
|
|
69
|
-
private_class_method :
|
|
94
|
+
private_class_method :virtual_rbs_entry
|
|
95
|
+
|
|
96
|
+
# The lazy-files run descriptor {RbsDescriptor.build_run} returns. Exposes the three readers the
|
|
97
|
+
# run-diagnostics cache consults — `gems` + `configs` are supplied eagerly (they feed the cache KEY,
|
|
98
|
+
# and are cheap); `files` (the RBS signature-tree digests, read only on a MISS) is computed once on
|
|
99
|
+
# first access and memoised, so a warm HIT never pays for it.
|
|
100
|
+
class RunDescriptor
|
|
101
|
+
attr_reader :gems, :configs
|
|
102
|
+
|
|
103
|
+
def initialize(loader:, gems:, configs:)
|
|
104
|
+
@loader = loader
|
|
105
|
+
@gems = gems
|
|
106
|
+
@configs = configs
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def files
|
|
110
|
+
# ADR-87 WD1 — this descriptor is validated (never a cache key), so the RBS signature tree rides the
|
|
111
|
+
# stat-then-digest `:stat` tier: a warm run stat-checks the (large, vendored) tree instead of
|
|
112
|
+
# re-hashing it.
|
|
113
|
+
@files ||= RbsDescriptor.file_entries(@loader, comparator: :stat)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
70
116
|
end
|
|
71
117
|
end
|
|
72
118
|
end
|
|
@@ -17,6 +17,20 @@ require "rbs"
|
|
|
17
17
|
#
|
|
18
18
|
# Idempotent: the guard checks `method_defined?(:_dump)` so requiring this file twice (or against an upstream
|
|
19
19
|
# rbs that adds Marshal hooks itself) is a no-op.
|
|
20
|
+
#
|
|
21
|
+
# `RBS::TypeName` / `RBS::Namespace` carry a second, subtler Marshal hazard, introduced by rbs 4.1's
|
|
22
|
+
# flyweight interning (ruby/rbs#2957): both memoise their `#hash` into an `@hash` ivar, and that value is
|
|
23
|
+
# derived from `Array#hash` / `Symbol#hash`, which Ruby seeds PER PROCESS. Marshal round-trips the ivar
|
|
24
|
+
# verbatim, so a cached `TypeName` loaded in a later process answers `hash` with the *writing* process's
|
|
25
|
+
# value while a freshly parsed `TypeName.parse("::String")` answers with this process's. The two are still
|
|
26
|
+
# `eql?`, so nothing raises — every Hash keyed by a TypeName simply misses. `RBS::Environment#class_decls`
|
|
27
|
+
# is exactly such a Hash, so a warm cache silently reported every core class as unknown.
|
|
28
|
+
#
|
|
29
|
+
# `_dump` / `_load` on both classes fixes it at the representation: the name is dumped as its source string
|
|
30
|
+
# and reconstructed through `.parse`, which routes back through the flyweight interner. That drops the stale
|
|
31
|
+
# `@hash` (it is re-memoised on demand, under this process's seed), and restores the flyweight identity the
|
|
32
|
+
# interner exists to provide — a cached env now shares one object per distinct name rather than one per
|
|
33
|
+
# reference. Both classes are value objects fully described by `to_s`, so the round-trip is lossless.
|
|
20
34
|
module RBS
|
|
21
35
|
class Location
|
|
22
36
|
unless method_defined?(:_dump)
|
|
@@ -29,4 +43,28 @@ module RBS
|
|
|
29
43
|
end
|
|
30
44
|
end
|
|
31
45
|
end
|
|
46
|
+
|
|
47
|
+
class Namespace
|
|
48
|
+
unless method_defined?(:_dump)
|
|
49
|
+
def _dump(_)
|
|
50
|
+
to_s
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self._load(string)
|
|
54
|
+
parse(string)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class TypeName
|
|
60
|
+
unless method_defined?(:_dump)
|
|
61
|
+
def _dump(_)
|
|
62
|
+
to_s
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self._load(string)
|
|
66
|
+
parse(string)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
32
70
|
end
|
data/lib/rigor/cache/store.rb
CHANGED
|
@@ -37,18 +37,10 @@ module Rigor
|
|
|
37
37
|
# blob whose class layout still happens to unmarshal.
|
|
38
38
|
PAYLOAD_ABI_VERSION = Rigor::VERSION
|
|
39
39
|
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
|
|
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
|
|
40
|
+
# The `generation_cap:` value declaring that a producer keeps MANY live entries under one id at once
|
|
41
|
+
# (per-file and per-plugin producers): a generation count is not a staleness proxy there, so the
|
|
42
|
+
# compaction pass in {#evict!} leaves the producer alone and only the size-based LRU pass can touch it.
|
|
43
|
+
UNBOUNDED_GENERATIONS = :unbounded
|
|
52
44
|
|
|
53
45
|
STALE_TEMP_FILE_AGE_SECONDS = 60 * 60
|
|
54
46
|
|
|
@@ -76,6 +68,14 @@ module Rigor
|
|
|
76
68
|
@misses = 0
|
|
77
69
|
@writes = 0
|
|
78
70
|
@by_producer = Hash.new { |h, k| h[k] = { hits: 0, misses: 0, writes: 0 } }
|
|
71
|
+
# `producer_id => generation cap`, populated from the `generation_cap:` every fetch call declares (see
|
|
72
|
+
# {#declare_generation_cap}). This is the bridge between the id STRINGS the fetch API takes and the
|
|
73
|
+
# directory names {#evict!} walks: a producer id the Store never saw declared stays uncapped, which
|
|
74
|
+
# errs toward under-evicting. Per-instance rather than process-global on purpose — a global mutable
|
|
75
|
+
# registry would be a `Ractor` shareability hazard for pool mode, and the Store instance that wrote a
|
|
76
|
+
# generation is the one whose `evict!` compacts it (`CLI::CheckCommand` calls
|
|
77
|
+
# `runner.cache_store&.evict!`).
|
|
78
|
+
@generation_caps = {}
|
|
79
79
|
# Process-level in-memory layer keyed by `(producer_id, cache_key)`. Avoids the disk read +
|
|
80
80
|
# `Marshal.load` cost (the dominant share of repeated cache-hit calls per stackprof) when many
|
|
81
81
|
# short-lived `Analysis::Runner` instances share one `Store` — the spec process, the LSP daemon's
|
|
@@ -161,6 +161,12 @@ module Rigor
|
|
|
161
161
|
private_class_method :collect_producers
|
|
162
162
|
|
|
163
163
|
# @param producer_id [String] stable cache namespace; only `[a-z][a-z0-9._-]*` is accepted.
|
|
164
|
+
# @param generation_cap [Integer, Symbol] how many generations of this producer survive a compaction
|
|
165
|
+
# pass — a positive `Integer` for a whole-project producer (one live entry, older ones unreachable),
|
|
166
|
+
# or {UNBOUNDED_GENERATIONS} for a producer with many simultaneously-live entries. REQUIRED, and
|
|
167
|
+
# sourced from the producer's own declaration (`RbsCacheProducer.generation_cap`,
|
|
168
|
+
# `RunCacheKey::GENERATION_CAP`, `Plugin::Base.producer generation_cap:`) rather than invented at the
|
|
169
|
+
# call site. See {#evict!}.
|
|
164
170
|
# @param params [Hash] producer inputs; mixed into the cache key via {Descriptor#cache_key_for}.
|
|
165
171
|
# @param descriptor [Rigor::Cache::Descriptor] the invalidation descriptor for the value being cached.
|
|
166
172
|
# @param serialize [#call, nil] optional callable that turns the producer's return value into a binary
|
|
@@ -175,9 +181,10 @@ module Rigor
|
|
|
175
181
|
# path.
|
|
176
182
|
# @yieldreturn the value to cache.
|
|
177
183
|
# @return the cached value (loaded from disk on hit; produced by the block on miss).
|
|
178
|
-
def fetch_or_compute(producer_id:, params:, descriptor:,
|
|
184
|
+
def fetch_or_compute(producer_id:, params:, descriptor:, generation_cap:,
|
|
179
185
|
serialize: nil, deserialize: nil, &block)
|
|
180
186
|
validate_producer_id!(producer_id)
|
|
187
|
+
declare_generation_cap(producer_id, generation_cap)
|
|
181
188
|
disk = ensure_schema_version!
|
|
182
189
|
|
|
183
190
|
key = descriptor.cache_key_for(producer_id: producer_id, params: params)
|
|
@@ -217,17 +224,20 @@ module Rigor
|
|
|
217
224
|
#
|
|
218
225
|
# The block MUST return `[value, dependency_descriptor]`. Disk reads are not in-process-memoised —
|
|
219
226
|
# validation always re-checks the filesystem — but a single run only looks up once.
|
|
220
|
-
|
|
227
|
+
#
|
|
228
|
+
# `generation_cap:` carries the same producer-declared compaction budget as {#fetch_or_compute}.
|
|
229
|
+
def fetch_or_validate(producer_id:, key_descriptor:, generation_cap:, params: {},
|
|
230
|
+
serialize: nil, deserialize: nil)
|
|
221
231
|
validate_producer_id!(producer_id)
|
|
232
|
+
declare_generation_cap(producer_id, generation_cap)
|
|
222
233
|
disk = ensure_schema_version!
|
|
223
234
|
|
|
224
235
|
key = key_descriptor.cache_key_for(producer_id: producer_id, params: params)
|
|
225
236
|
path = disk ? entry_path(producer_id, key) : nil
|
|
226
237
|
cached = path && read_entry(path, deserialize: deserialize)
|
|
227
|
-
if
|
|
228
|
-
pair[1].is_a?(Descriptor) && pair[1].fresh?
|
|
238
|
+
if (validated = fresh_pair_value(cached))
|
|
229
239
|
@monitor.synchronize { record(:hits, producer_id) }
|
|
230
|
-
return
|
|
240
|
+
return validated[0]
|
|
231
241
|
end
|
|
232
242
|
|
|
233
243
|
value, dependency_descriptor = block_given? ? yield : [nil, Descriptor.new]
|
|
@@ -239,8 +249,36 @@ module Rigor
|
|
|
239
249
|
value
|
|
240
250
|
end
|
|
241
251
|
|
|
242
|
-
# ADR-
|
|
243
|
-
#
|
|
252
|
+
# ADR-87 WD4 — the READ half of {#fetch_or_validate} with no compute and no write: returns the cached
|
|
253
|
+
# value on a fresh hit, nil on a miss / stale / unavailable-disk. The boot-slimming hit probe calls this
|
|
254
|
+
# to serve a run's diagnostics WITHOUT loading the inference engine — it never runs a producer block, so
|
|
255
|
+
# there is nothing to write. Records a hit (for `--cache-stats` parity) but never a miss (a probe miss
|
|
256
|
+
# hands off to the full path, which records its own).
|
|
257
|
+
#
|
|
258
|
+
# Takes no `generation_cap:`: a pure read creates no generation, so a run that only ever peeks has
|
|
259
|
+
# nothing to compact. The write path for the same producer declares the cap.
|
|
260
|
+
def peek_validated(producer_id:, key_descriptor:, params: {}, deserialize: nil)
|
|
261
|
+
validate_producer_id!(producer_id)
|
|
262
|
+
return nil unless ensure_schema_version!
|
|
263
|
+
|
|
264
|
+
key = key_descriptor.cache_key_for(producer_id: producer_id, params: params)
|
|
265
|
+
cached = read_entry(entry_path(producer_id, key), deserialize: deserialize)
|
|
266
|
+
validated = fresh_pair_value(cached)
|
|
267
|
+
return nil if validated.nil?
|
|
268
|
+
|
|
269
|
+
@monitor.synchronize { record(:hits, producer_id) }
|
|
270
|
+
validated[0]
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# ADR-6 § "Eviction" — compaction pass over the on-disk cache. No-op when the store is read-only.
|
|
274
|
+
#
|
|
275
|
+
# The generation cap of pass 2 comes from what the producers themselves declared through this Store's
|
|
276
|
+
# fetch calls (`generation_cap:`), NOT from a maintained table of producer ids: a producer id this
|
|
277
|
+
# Store never saw is left alone. That is deliberately the safe direction — a producer that was not
|
|
278
|
+
# consulted this run also wrote no new generation, so the only entries this can skip are ones that were
|
|
279
|
+
# already sitting there.
|
|
280
|
+
#
|
|
281
|
+
# Stale temp file cleanup and the generation cap run regardless of `max_bytes:` — they reclaim
|
|
244
282
|
# provably-dead bytes (leaked temp files, unreachable content-keyed generations) rather than enforcing a
|
|
245
283
|
# size budget, so an explicitly unbounded store (`max_bytes: nil`) still benefits from them. The
|
|
246
284
|
# size-based LRU pass below stays gated on `max_bytes:` being configured: it walks all remaining
|
|
@@ -262,7 +300,7 @@ module Rigor
|
|
|
262
300
|
entries.each do |entry|
|
|
263
301
|
break if total <= @max_bytes
|
|
264
302
|
|
|
265
|
-
total -= entry[:bytes] if
|
|
303
|
+
total -= entry[:bytes] if unlink_entry_and_shard?(entry[:path])
|
|
266
304
|
end
|
|
267
305
|
nil
|
|
268
306
|
rescue StandardError
|
|
@@ -274,6 +312,19 @@ module Rigor
|
|
|
274
312
|
Entry = Data.define(:descriptor_bytes, :value)
|
|
275
313
|
private_constant :Entry
|
|
276
314
|
|
|
315
|
+
# A record-and-validate entry is a fresh hit iff it deserialised to a `[value, dependency_descriptor]`
|
|
316
|
+
# pair whose descriptor still validates against the filesystem. Returns the pair on a fresh hit, nil
|
|
317
|
+
# otherwise. Shared by {#fetch_or_validate} and {#peek_validated} so both apply identical hit criteria.
|
|
318
|
+
def fresh_pair_value(cached)
|
|
319
|
+
return nil if cached.nil?
|
|
320
|
+
|
|
321
|
+
pair = cached.value
|
|
322
|
+
return nil unless pair.is_a?(Array) && pair.size == 2 &&
|
|
323
|
+
pair[1].is_a?(Descriptor) && pair[1].fresh?
|
|
324
|
+
|
|
325
|
+
pair
|
|
326
|
+
end
|
|
327
|
+
|
|
277
328
|
def record(counter, producer_id)
|
|
278
329
|
case counter
|
|
279
330
|
when :hits then @hits += 1
|
|
@@ -290,6 +341,37 @@ module Rigor
|
|
|
290
341
|
"producer_id must match #{VALID_PRODUCER_ID.inspect}, got #{producer_id.inspect}"
|
|
291
342
|
end
|
|
292
343
|
|
|
344
|
+
# Records the producer's declared compaction budget for {#evict!}. Every fetch call carries it, so a
|
|
345
|
+
# producer cannot reach disk without stating one — the failure mode the previous hardcoded id table
|
|
346
|
+
# had (a new whole-project producer silently uncapped) is not expressible.
|
|
347
|
+
#
|
|
348
|
+
# Two DIFFERENT caps for one producer id in one process is a producer bug, not a policy: whichever the
|
|
349
|
+
# compaction pass picked would be arbitrary. It raises, consistent with the serializer-contract
|
|
350
|
+
# violations {#try_write_entry} deliberately lets through.
|
|
351
|
+
def declare_generation_cap(producer_id, generation_cap)
|
|
352
|
+
validate_generation_cap!(producer_id, generation_cap)
|
|
353
|
+
@monitor.synchronize do
|
|
354
|
+
previous = @generation_caps[producer_id]
|
|
355
|
+
if !previous.nil? && previous != generation_cap
|
|
356
|
+
raise ArgumentError,
|
|
357
|
+
"producer #{producer_id.inspect} declared generation_cap #{generation_cap.inspect} after " \
|
|
358
|
+
"#{previous.inspect}; one producer id must declare one cap"
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
@generation_caps[producer_id] = generation_cap
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def validate_generation_cap!(producer_id, generation_cap)
|
|
366
|
+
return if generation_cap == UNBOUNDED_GENERATIONS
|
|
367
|
+
return if generation_cap.is_a?(Integer) && generation_cap.positive?
|
|
368
|
+
|
|
369
|
+
raise ArgumentError,
|
|
370
|
+
"producer #{producer_id.inspect} must declare generation_cap as a positive Integer (a " \
|
|
371
|
+
"whole-project producer: how many generations survive compaction) or " \
|
|
372
|
+
"#{UNBOUNDED_GENERATIONS.inspect} (many entries live at once), got #{generation_cap.inspect}"
|
|
373
|
+
end
|
|
374
|
+
|
|
293
375
|
def entry_path(producer_id, key)
|
|
294
376
|
File.join(@root, producer_id, key[0, 2], "#{key[2..]}.entry")
|
|
295
377
|
end
|
|
@@ -511,7 +593,7 @@ module Rigor
|
|
|
511
593
|
next unless File.file?(path)
|
|
512
594
|
next if File.mtime(path) > cutoff
|
|
513
595
|
|
|
514
|
-
|
|
596
|
+
unlink_entry_and_shard?(path)
|
|
515
597
|
rescue StandardError
|
|
516
598
|
next
|
|
517
599
|
end
|
|
@@ -520,16 +602,16 @@ module Rigor
|
|
|
520
602
|
end
|
|
521
603
|
|
|
522
604
|
def evict_excess_generations(entries)
|
|
605
|
+
caps = @monitor.synchronize { @generation_caps.dup }
|
|
523
606
|
removed = {}
|
|
524
607
|
entries.group_by { |entry| entry[:producer] }.each do |producer, producer_entries|
|
|
525
|
-
cap =
|
|
608
|
+
cap = caps[producer]
|
|
609
|
+
cap = nil if cap == UNBOUNDED_GENERATIONS
|
|
526
610
|
next if cap.nil? || producer_entries.size <= cap
|
|
527
611
|
|
|
528
|
-
producer_entries.sort_by { |entry| [entry[:mtime], entry[:path]] }
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
removed[entry[:path]] = true if unlink_entry(entry[:path])
|
|
532
|
-
end
|
|
612
|
+
excess = producer_entries.sort_by { |entry| [entry[:mtime], entry[:path]] }
|
|
613
|
+
.first(producer_entries.size - cap)
|
|
614
|
+
excess.each { |entry| removed[entry[:path]] = true if unlink_entry_and_shard?(entry[:path]) }
|
|
533
615
|
end
|
|
534
616
|
return entries if removed.empty?
|
|
535
617
|
|
|
@@ -543,6 +625,29 @@ module Rigor
|
|
|
543
625
|
false
|
|
544
626
|
end
|
|
545
627
|
|
|
628
|
+
# {#unlink_entry} plus a best-effort {#rmdir_if_empty} of the shard directory the unlinked file left
|
|
629
|
+
# behind. Shared by every eviction/sweep pass so an emptied shard is cleaned up wherever an entry (or
|
|
630
|
+
# stale temp file) is removed, per the issue #216 fossil fix.
|
|
631
|
+
def unlink_entry_and_shard?(path)
|
|
632
|
+
return false unless unlink_entry(path)
|
|
633
|
+
|
|
634
|
+
rmdir_if_empty(File.dirname(path))
|
|
635
|
+
true
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
# Removes `dir` — a shard directory (`entry_path`'s `key[0, 2]` component) — when the unlink that
|
|
639
|
+
# just preceded this call was the one to empty it. Best-effort and rescued the same way
|
|
640
|
+
# {#unlink_entry} is: a concurrent writer's `FileUtils.mkdir_p` recreating the shard between the
|
|
641
|
+
# unlink and this call (`Errno::ENOTEMPTY`) or another pass already having removed it
|
|
642
|
+
# (`Errno::ENOENT`) are both benign outcomes, never a reason to break the eviction/sweep pass. This
|
|
643
|
+
# is purely cosmetic (inode reclaim) — it never decides which ENTRIES are evicted, only tidies the
|
|
644
|
+
# directory left behind once they are gone.
|
|
645
|
+
def rmdir_if_empty(dir)
|
|
646
|
+
Dir.rmdir(dir)
|
|
647
|
+
rescue StandardError
|
|
648
|
+
nil
|
|
649
|
+
end
|
|
650
|
+
|
|
546
651
|
# Returns an array of `{ path:, producer:, mtime:, bytes: }` hashes for every `.entry` file under the
|
|
547
652
|
# cache root, skipping unreadable entries.
|
|
548
653
|
def collect_entry_stats
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
# Runtime CI-environment detection (ADR-51 WD7), modelled on `OndraM/ci-detector` (the library PHPStan uses). Reads
|
|
5
|
+
# the well-known environment variables a CI provider sets and returns the matching {Platform}, classifying it into a
|
|
6
|
+
# **tier** that decides how `rigor check` surfaces diagnostics there:
|
|
7
|
+
#
|
|
8
|
+
# :native_stdout — Rigor has a native format that renders purely from
|
|
9
|
+
# stdout, so it is auto-emitted on top of the human
|
|
10
|
+
# output (GitHub Actions → `github`, TeamCity →
|
|
11
|
+
# `teamcity`). These are the first-class platforms.
|
|
12
|
+
# :native_artifact — Rigor has a native format but it needs a CI-wired
|
|
13
|
+
# report artifact, not stdout (GitLab CI → `gitlab`).
|
|
14
|
+
# First-class, but Rigor only *hints* the format.
|
|
15
|
+
# :reviewdog — no native Rigor format; second-class, routed through
|
|
16
|
+
# reviewdog (`checkstyle`/`sarif`) or `junit`. Hint
|
|
17
|
+
# only.
|
|
18
|
+
#
|
|
19
|
+
# Lives outside the CLI namespace because it is a pure function of the environment hash with no CLI dependency, and
|
|
20
|
+
# non-CLI consumers exist: {Configuration#cache_validation_strict?} resolves the `auto` cache-validation default
|
|
21
|
+
# through it (a fresh CI checkout regenerates every stat tuple, so ADR-87's stat tier cannot work there).
|
|
22
|
+
# Detection is fully testable; callers pass `ENV`.
|
|
23
|
+
# `RIGOR_CI_DETECT=0` (or `false`/`no`) disables it globally — the seam the spec suite uses for determinism.
|
|
24
|
+
module CiDetector
|
|
25
|
+
Platform = Struct.new(:id, :name, :format, :tier, keyword_init: true) do
|
|
26
|
+
def native_stdout? = tier == :native_stdout
|
|
27
|
+
def native_artifact? = tier == :native_artifact
|
|
28
|
+
def reviewdog? = tier == :reviewdog
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The detection table, ordered most-specific first so the generic `CI=true` catch-all is last (a provider that
|
|
32
|
+
# also sets `CI` is still recognised by its own variable). `match` is `:truthy` (value in 1/true/yes/on),
|
|
33
|
+
# `:present` (variable set non-empty), or `:equals`.
|
|
34
|
+
PROVIDERS = [
|
|
35
|
+
{ id: "github-actions", name: "GitHub Actions", format: "github", tier: :native_stdout,
|
|
36
|
+
var: "GITHUB_ACTIONS", match: :truthy },
|
|
37
|
+
{ id: "gitlab", name: "GitLab CI", format: "gitlab", tier: :native_artifact,
|
|
38
|
+
var: "GITLAB_CI", match: :truthy },
|
|
39
|
+
{ id: "teamcity", name: "TeamCity", format: "teamcity", tier: :native_stdout,
|
|
40
|
+
var: "TEAMCITY_VERSION", match: :present },
|
|
41
|
+
{ id: "circleci", name: "CircleCI", format: nil, tier: :reviewdog,
|
|
42
|
+
var: "CIRCLECI", match: :truthy },
|
|
43
|
+
{ id: "jenkins", name: "Jenkins", format: nil, tier: :reviewdog,
|
|
44
|
+
var: "JENKINS_URL", match: :present },
|
|
45
|
+
{ id: "travis", name: "Travis CI", format: nil, tier: :reviewdog,
|
|
46
|
+
var: "TRAVIS", match: :truthy },
|
|
47
|
+
{ id: "appveyor", name: "AppVeyor", format: nil, tier: :reviewdog,
|
|
48
|
+
var: "APPVEYOR", match: :truthy },
|
|
49
|
+
{ id: "azure-pipelines", name: "Azure Pipelines", format: nil, tier: :reviewdog,
|
|
50
|
+
var: "TF_BUILD", match: :present },
|
|
51
|
+
{ id: "bitbucket", name: "Bitbucket Pipelines", format: nil, tier: :reviewdog,
|
|
52
|
+
var: "BITBUCKET_BUILD_NUMBER", match: :present },
|
|
53
|
+
{ id: "buildkite", name: "Buildkite", format: nil, tier: :reviewdog,
|
|
54
|
+
var: "BUILDKITE", match: :truthy },
|
|
55
|
+
{ id: "drone", name: "Drone CI", format: nil, tier: :reviewdog,
|
|
56
|
+
var: "DRONE", match: :truthy },
|
|
57
|
+
{ id: "semaphore", name: "Semaphore", format: nil, tier: :reviewdog,
|
|
58
|
+
var: "SEMAPHORE", match: :truthy },
|
|
59
|
+
{ id: "codeship", name: "Codeship", format: nil, tier: :reviewdog,
|
|
60
|
+
var: "CI_NAME", match: :equals, value: "codeship" },
|
|
61
|
+
{ id: "ci", name: "CI", format: nil, tier: :reviewdog,
|
|
62
|
+
var: "CI", match: :truthy }
|
|
63
|
+
].freeze
|
|
64
|
+
|
|
65
|
+
module_function
|
|
66
|
+
|
|
67
|
+
# Returns the detected {Platform}, or nil when no CI is recognised or detection is disabled via `RIGOR_CI_DETECT`.
|
|
68
|
+
def detect(env = ENV)
|
|
69
|
+
return nil if disabled?(env)
|
|
70
|
+
|
|
71
|
+
row = PROVIDERS.find { |provider| matches?(env, provider) }
|
|
72
|
+
return nil if row.nil?
|
|
73
|
+
|
|
74
|
+
Platform.new(id: row[:id], name: row[:name], format: row[:format], tier: row[:tier])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def disabled?(env)
|
|
78
|
+
%w[0 false no off].include?(env["RIGOR_CI_DETECT"].to_s.strip.downcase)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def matches?(env, provider)
|
|
82
|
+
value = env[provider[:var]].to_s.strip
|
|
83
|
+
case provider[:match]
|
|
84
|
+
when :truthy then %w[1 true yes on].include?(value.downcase)
|
|
85
|
+
when :present then !value.empty?
|
|
86
|
+
when :equals then value.downcase == provider[:value]
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|