Class: Rigor::Analysis::PluginFactFingerprint
- Inherits:
-
Object
- Object
- Rigor::Analysis::PluginFactFingerprint
- Defined in:
- lib/rigor/analysis/plugin_fact_fingerprint.rb
Overview
ADR-88 WD1 — a stable fingerprint of the plugin FACT SURFACE for an incremental run: the plugin-computed
values a cached per-file diagnostic can depend on but that the snapshot's global fingerprint (config /
gems / RBS env / signature_paths:) does NOT capture.
The gap: a plugin like rigor-sorbet reads .rb / .rbi sig files under its own paths: / rbi_paths:
(outside signature_paths:), builds a catalog, and contributes dynamic_return types at call sites in
OTHER files. An edit to such a sig file changes the types those call sites resolve WITHOUT moving any
analyzed file's content — so IncrementalSnapshot.fingerprint stays fresh, the recheck sees an empty
changed set, and it serves the stale cached diagnostics. Nothing in the ADR-46 dependency graph records
the cross-file plugin read (the catalog is plugin-internal, not a Scope table).
This class digests three fact-surface channels after plugin #prepare:
(a) every ADR-9 fact-store publication `(plugin_id, name) -> value` (a producer/consumer fact such as
`activerecord :model_index`, `dry-types :dry_type_aliases`),
(b) every declared ADR-60 producer's computed value (`sorbet :catalog`, `actionpack :controller_index`),
(c) each plugin's optional {Plugin::Base#incremental_state_fingerprint} — a hook for internal catalog
state that lives in neither (a) nor (b).
The digest rides the Cache::IncrementalSnapshot (schema 9); a warm recheck compares it and, on a mismatch, invalidates the snapshot and runs a full analysis (the conservative, sound direction).
OPAQUE plugins: a plugin that CONTRIBUTES per-call types (dynamic_return / narrowing_facts) but
declares NONE of (a)/(b)/(c) has stale-able state the fingerprint cannot see. Rather than silently risk a
stale reuse, such a plugin makes the snapshot un-reusable (a full analysis every run) and is named in a
one-line note. The bundled contributing plugins all declare a surface (producers, facts, or the hook), so
the bundled set stays incremental-capable; a third-party plugin that contributes types must do the same.
The probe is ALWAYS sequential and independent of the analysis's pool mode, so the invalidation decision is identical whether the recheck ran pooled or sequential (the pooled-vs-sequential parity requirement).
Defined Under Namespace
Classes: Result
Class Method Summary collapse
-
.compute(configuration:, cache_store:, plugin_requirer: nil) ⇒ Object
The SEQUENTIAL probe: load the plugins and run
#preparefresh, then fingerprint. -
.from_registry(registry) ⇒ Object
ADR-88 WD1 — the CHEAP post-hoc path: fingerprint a registry the analysis runner ALREADY prepared (sequential runs run
#prepareand consult the producers during analysis, so producer values are memoised and their cache entries current). -
.key_digest(registry) ⇒ String?
The fact surface reduced to the one String a cache KEY can carry, or nil when the surface cannot be seen at all (an opaque plugin — one that contributes call-site types while declaring none of the three fingerprint channels).
-
.prepared_registry(configuration:, cache_store:, plugin_requirer:) ⇒ Object
Loads the plugins and runs every
#preparehook sequentially, returning the prepared registry (nil on any failure → the caller treats it as "no fact surface").
Instance Method Summary collapse
Class Method Details
.compute(configuration:, cache_store:, plugin_requirer: nil) ⇒ Object
The SEQUENTIAL probe: load the plugins and run #prepare fresh, then fingerprint. Pool-independent
(its prepare pass is always sequential), so it is the pooled-mode path and the reference the parity
spec asserts against.
58 59 60 61 62 |
# File 'lib/rigor/analysis/plugin_fact_fingerprint.rb', line 58 def self.compute(configuration:, cache_store:, plugin_requirer: nil) new.digest_registry( prepared_registry(configuration: configuration, cache_store: cache_store, plugin_requirer: plugin_requirer) ) end |
.from_registry(registry) ⇒ Object
ADR-88 WD1 — the CHEAP post-hoc path: fingerprint a registry the analysis runner ALREADY prepared
(sequential runs run #prepare and consult the producers during analysis, so producer values are
memoised and their cache entries current). Avoids a second #prepare pass and a second producer
validation. Used only when the runner's main-process registry is prepared (sequential); the pooled path
(whose main process skips #prepare) falls back to compute. Both compute the identical digest for a
given fact surface, so the reuse decision is pool-independent.
70 71 72 |
# File 'lib/rigor/analysis/plugin_fact_fingerprint.rb', line 70 def self.from_registry(registry) new.digest_registry(registry) end |
.key_digest(registry) ⇒ String?
The fact surface reduced to the one String a cache KEY can carry, or nil when the surface cannot be seen at all (an opaque plugin — one that contributes call-site types while declaring none of the three fingerprint channels). A nil obliges the caller to decline caching entirely, which is the same conservative direction Rigor::Analysis::PluginFactFingerprint::Result#reusable_against? takes for the incremental snapshot: a key that silently omitted an invisible input would serve a stale value rather than miss.
Keeping the opaque decision here, rather than at each cache's call site, means a new consumer cannot
key on digest while forgetting that an opaque surface makes it meaningless.
83 84 85 86 |
# File 'lib/rigor/analysis/plugin_fact_fingerprint.rb', line 83 def self.key_digest(registry) result = from_registry(registry) result.opaque? ? nil : result.digest.to_s end |
.prepared_registry(configuration:, cache_store:, plugin_requirer:) ⇒ Object
Loads the plugins and runs every #prepare hook sequentially, returning the prepared registry (nil on
any failure → the caller treats it as "no fact surface").
90 91 92 93 94 95 96 97 |
# File 'lib/rigor/analysis/plugin_fact_fingerprint.rb', line 90 def self.prepared_registry(configuration:, cache_store:, plugin_requirer:) Runner::ProjectPrePasses.new( configuration: configuration, cache_store: cache_store, buffer: nil, plugin_requirer: plugin_requirer, pool_mode: -> { false } ).prepared_registry rescue StandardError nil end |
Instance Method Details
#digest_registry(registry) ⇒ Object
99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/rigor/analysis/plugin_fact_fingerprint.rb', line 99 def digest_registry(registry) return Result.new(digest: nil, opaque_plugin_ids: [].freeze) if registry.nil? || registry.empty? facts_by_plugin = facts_by_plugin(registry.plugins.first&.services&.fact_store) parts = fact_parts(facts_by_plugin) opaque = [] registry.plugins.each { |plugin| collect_plugin_parts(plugin, facts_by_plugin, parts, opaque) } # Sort the parts so the digest is independent of plugin registration / iteration order. Result.new( digest: Digest::SHA256.hexdigest(parts.sort.join("\x00")), opaque_plugin_ids: opaque.uniq.freeze ) end |