Class: Rigor::Analysis::PluginFactFingerprint

Inherits:
Object
  • Object
show all
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

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