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). -
.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 |
.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").
76 77 78 79 80 81 82 83 |
# File 'lib/rigor/analysis/plugin_fact_fingerprint.rb', line 76 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
85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/rigor/analysis/plugin_fact_fingerprint.rb', line 85 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 |