Class: Rigor::Effects::PluginFacts
- Inherits:
-
Object
- Object
- Rigor::Effects::PluginFacts
- Defined in:
- lib/rigor/effects/plugin_facts.rb
Overview
The loaded plugins' effect contributions, compiled once per process into the tables the scan and the snapshot read (ADR-103 WD2 / WD6 / WD10 / WD14; #387).
A plugin's manifest states its contribution declaratively — effect_labels:, effect_attributions:,
effect_edges:, effect_entry_points:, all frozen value objects. Nothing here re-reads a manifest at
scan time: the per-call lookup is two Hash reads and, for a class-name row, a walk up a superclass
table that is a Hash read per level.
Nothing builds one unless collection is on. Plugin::Registry#effect_contributions is itself
lazy — a plugin MAY compute its rows from project facts, and rigor-activejob does — and this class is
what turns the result into indices. PluginFacts.build is called from exactly one place on each side of the fork
boundary, behind configuration.effects_enabled?, so a rigor check with no effects: block neither
allocates one nor lets a plugin read a file for it.
Two matching rules, because the framework has two shapes
A class-name row (ActiveRecord::Base#save) matches through the project's inheritance chain:
user.save on a User < ApplicationRecord < ActiveRecord::Base is the whole point, and matching the
exact owner (which is what Catalog does, correctly, for Ruby's core) would find nothing. The chain
walked is the project's own superclass table — the cross-file discovery pre-pass's
discovered_superclasses, the same table ExpressionTyper walks for an unresolved implicit-self
call. It stops where the project stops, which is exactly right: ApplicationRecord < ActiveRecord::Base
is a line in app/models/application_record.rb, and a class that never names a framework base in
project source is not one.
A receiver-path row (Rails.cache#read) matches the receiver expression as written. Rails.cache
returns an adapter-dependent object by design — memory, Redis, the filesystem — so there is no receiver
class for a class-name row to key on, and the only stable handle is the path the programmer typed.
What is deliberately not consulted
The RBS ancestor chain. A gem's shipped signatures do carry class User < ApplicationRecord-shaped
ancestry when the project generates RBS for its models, but reading it here would make a row's reach a
function of whether the project happens to run rbs prototype — a plugin contribution that appears and
disappears with an unrelated tool. The project's own class … < lines are the fact; a project whose
models are declared only in RBS gets no plugin attribution and no taint, which is the fail-quiet
direction.
Defined Under Namespace
Constant Summary collapse
- ANCESTRY_CAP =
How far the superclass walk climbs before giving up. A project hierarchy deeper than this is either pathological or cyclic, and a cycle is possible in a project's as-written table.
24
Instance Attribute Summary collapse
-
#digest ⇒ Object
readonly
A content digest of every compiled plugin fact — labels, attributions, edges and presets, each with the plugin that contributed it.
-
#entry_points ⇒ Object
readonly
Every
effect_entry_points:preset across the loaded set, in registration order. -
#labels_by_owner ⇒ Object
readonly
{owner => [labels]}— what #extend_registry folds into the run's vocabulary. -
#warnings ⇒ Object
readonly
Human-readable notes about contributions that were accepted only in part — a third-party plugin's
discharge: truedemoted, a label root refused.
Class Method Summary collapse
Instance Method Summary collapse
-
#attributions? ⇒ Boolean
Whether any attribution row exists at all — the scan's fast path, asked once per call site.
-
#class_row(owner, singleton, selector) ⇒ Row?
The row colouring
owner'sselector, found onowneritself or on a project ancestor of it. -
#descends_from?(class_name, ancestor) ⇒ Boolean
Whether
class_nameisancestor, or reaches it through the project's ownclass … <lines. - #edges? ⇒ Boolean
-
#edges_for(target) ⇒ Object
The framework-edge strategies of one kind, e.g.
- #empty? ⇒ Boolean
-
#extend_registry(registry) ⇒ Object
registryextended with every plugin'seffect_labels:, each under its own owner so Registry#with enforces root ownership per plugin rather than for the set as a whole. -
#initialize(contributions:, superclasses:) ⇒ PluginFacts
constructor
A new instance of PluginFacts.
-
#path_row(path, selector) ⇒ Object
The row colouring
path'sselector, wherepathis a receiver expression ("Rails.cache"). -
#result_row(producer, selector) ⇒ Object
The row colouring
selectoron the RESULT of a call toproducer(or to a project ancestor of it):UserMailer.welcome(u).deliver_now,WelcomeJob.set(wait: 1.hour).perform_later. -
#self_path_row(path, selector, owner_class) ⇒ Object
The row colouring
path'sselectorfor a receiver rooted at implicit self ("self.flash.now"), inside a unit whose class isowner_class.
Constructor Details
#initialize(contributions:, superclasses:) ⇒ PluginFacts
Returns a new instance of PluginFacts.
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/rigor/effects/plugin_facts.rb', line 97 def initialize(contributions:, superclasses:) @warnings = [] @class_rows = {} @path_rows = {} @self_rows = {} @result_rows = {} @edges = [] @labels_by_owner = {} @entry_points = [] contributions.each { |contribution| absorb(contribution) } @superclasses = superclasses || NO_ROWS @ancestry = {} @digest = compute_digest finalize end |
Instance Attribute Details
#digest ⇒ Object (readonly)
A content digest of every compiled plugin fact — labels, attributions, edges and presets, each with
the plugin that contributed it. Identity folds it in, so upgrading a plugin whose rows moved
invalidates the effects slot exactly as a re-audited data/effects/core.yml row does. Deliberately
independent of the project's superclass table, which is a project input the diagnostics identity
already covers.
118 119 120 |
# File 'lib/rigor/effects/plugin_facts.rb', line 118 def digest @digest end |
#entry_points ⇒ Object (readonly)
Every effect_entry_points: preset across the loaded set, in registration order.
95 96 97 |
# File 'lib/rigor/effects/plugin_facts.rb', line 95 def entry_points @entry_points end |
#labels_by_owner ⇒ Object (readonly)
{owner => [labels]} — what #extend_registry folds into the run's vocabulary.
92 93 94 |
# File 'lib/rigor/effects/plugin_facts.rb', line 92 def labels_by_owner @labels_by_owner end |
#warnings ⇒ Object (readonly)
Human-readable notes about contributions that were accepted only in part — a third-party plugin's
discharge: true demoted, a label root refused. Surfaced by rigor effects; never a diagnostic,
because a plugin the user chose is not the project's mistake to be flagged for.
89 90 91 |
# File 'lib/rigor/effects/plugin_facts.rb', line 89 def warnings @warnings end |
Class Method Details
.build(plugin_registry, superclasses: NO_ROWS) ⇒ Object
77 78 79 80 81 82 83 84 |
# File 'lib/rigor/effects/plugin_facts.rb', line 77 def self.build(plugin_registry, superclasses: NO_ROWS) contributions = plugin_registry&.effect_contributions || [] return empty if contributions.empty? new(contributions: contributions, superclasses: superclasses) rescue StandardError empty end |
.empty ⇒ Object
69 70 71 |
# File 'lib/rigor/effects/plugin_facts.rb', line 69 def self.empty @empty ||= new(contributions: [], superclasses: NO_ROWS) end |
Instance Method Details
#attributions? ⇒ Boolean
Whether any attribution row exists at all — the scan's fast path, asked once per call site.
125 126 127 |
# File 'lib/rigor/effects/plugin_facts.rb', line 125 def attributions? !@class_rows.empty? || !@path_rows.empty? || !@self_rows.empty? || !@result_rows.empty? end |
#class_row(owner, singleton, selector) ⇒ Row?
The row colouring owner's selector, found on owner itself or on a project ancestor of it.
138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/rigor/effects/plugin_facts.rb', line 138 def class_row(owner, singleton, selector) return nil if owner.nil? || @class_rows.empty? bucket = @class_rows[singleton] return nil if bucket.nil? ancestry(owner).each do |candidate| row = bucket[candidate]&.[](selector) return row if row end nil end |
#descends_from?(class_name, ancestor) ⇒ Boolean
Whether class_name is ancestor, or reaches it through the project's own class … < lines.
193 194 195 196 197 |
# File 'lib/rigor/effects/plugin_facts.rb', line 193 def descends_from?(class_name, ancestor) return false if class_name.nil? ancestry(class_name).include?(ancestor) end |
#edges? ⇒ Boolean
129 130 131 |
# File 'lib/rigor/effects/plugin_facts.rb', line 129 def edges? !@edges.empty? end |
#edges_for(target) ⇒ Object
The framework-edge strategies of one kind, e.g. every :activerecord_callbacks base class the
loaded plugins named.
188 189 190 |
# File 'lib/rigor/effects/plugin_facts.rb', line 188 def edges_for(target) @edges.select { |edge| edge.target == target } end |
#empty? ⇒ Boolean
120 121 122 |
# File 'lib/rigor/effects/plugin_facts.rb', line 120 def empty? !attributions? && @edges.empty? && @labels_by_owner.empty? && @entry_points.empty? end |
#extend_registry(registry) ⇒ Object
registry extended with every plugin's effect_labels:, each under its own owner so
Registry#with enforces root ownership per plugin rather than for the set as a whole. A refusal is
recorded as a warning and that plugin's labels are dropped; the rest of the run keeps its
vocabulary, because one plugin overreaching must not un-name another's labels.
203 204 205 206 207 208 209 210 |
# File 'lib/rigor/effects/plugin_facts.rb', line 203 def extend_registry(registry) @labels_by_owner.each do |owner, labels| registry = registry.with(labels: labels, owner: owner) rescue Registry::Error => e @warnings << "effect labels from #{owner.inspect} were not registered: #{e.}" end registry end |
#path_row(path, selector) ⇒ Object
The row colouring path's selector, where path is a receiver expression ("Rails.cache").
Exact — a receiver path names one object and has no ancestry to walk.
153 154 155 156 157 |
# File 'lib/rigor/effects/plugin_facts.rb', line 153 def path_row(path, selector) return nil if path.nil? || @path_rows.empty? @path_rows[path]&.[](selector) end |
#result_row(producer, selector) ⇒ Object
The row colouring selector on the RESULT of a call to producer (or to a project ancestor of it):
UserMailer.welcome(u).deliver_now, WelcomeJob.set(wait: 1.hour).perform_later. The lazy object
in between has no declared type; the class that made it is written in the source.
176 177 178 179 180 181 182 183 184 |
# File 'lib/rigor/effects/plugin_facts.rb', line 176 def result_row(producer, selector) return nil if producer.nil? || @result_rows.empty? ancestry(producer).each do |candidate| row = @result_rows[candidate]&.[](selector) return row if row end nil end |
#self_path_row(path, selector, owner_class) ⇒ Object
The row colouring path's selector for a receiver rooted at implicit self ("self.flash.now"),
inside a unit whose class is owner_class. Answers nil when the row's within: class is not on
owner_class's project ancestry — a receiver-less session outside a controller is a different
session.
163 164 165 166 167 168 169 170 171 |
# File 'lib/rigor/effects/plugin_facts.rb', line 163 def self_path_row(path, selector, owner_class) return nil if path.nil? || @self_rows.empty? row = @self_rows[path]&.[](selector) return nil if row.nil? return nil unless descends_from?(owner_class, row.within) row end |