Module: Rigor::Effects::EnvelopeCheck
- Defined in:
- lib/rigor/effects/envelope_check.rb
Overview
Judges each method's declared envelope against what the run actually proved (ADR-103 WD1 / WD8;
#383). The one place effect.envelope-exceeded is decided.
Three rules make this FP-safe, and each is load-bearing:
- It reads the PROVEN lane only, never the declared one and never taint. A non-exhaustive summary reads "these effects, and possibly more" and contributes no finding of its own; what it did prove is still proven, so a proven label outside the bound still fires. That is "as strict as proven" (robustness-principle.md), and it is why an unresolved call can never manufacture one.
- The bound binds the method's CODE, transitively. The envelope is a contract about what the
method does, and a repository that calls a helper that calls
Net::HTTPdoes perform HTTP. So the comparison is against EffectTable::Entry#proven — the fixpoint's closure over project callees — not against the unit's own direct summary. mutate.localis tolerated by every envelope,%a{pure}included (Rigor::Effects::Envelope#tolerates?).
effects.tolerated: discharges, per origin (#385). The comparison reads
EffectTable::Entry#undischarged rather than proven: the propagator has already dropped every
origin bundle the policy discharges and closed the rest over the graph, so a pure-declared method
that logs is silent under tolerated: [telemetry] while the File.read two lines down still fires.
apply_tolerated: false — --no-tolerated-effects — judges against proven instead, which is the
audit switch that makes the policy inspectable rather than invisible.
Defined Under Namespace
Class Method Summary collapse
-
.distribute(table, method_envelopes, class_envelopes, config_envelopes) ⇒ Object
Resolves the per-method envelope for every unit the table knows, nearest wins:.
-
.run(table:, method_envelopes:, class_envelopes:, config_envelopes: {}, positions: Positions.empty, apply_tolerated: true) ⇒ Array<Finding>
Sorted by position then key then label, so a run explains identically twice.
Class Method Details
.distribute(table, method_envelopes, class_envelopes, config_envelopes) ⇒ Object
Resolves the per-method envelope for every unit the table knows, nearest wins:
per-method annotation > class-level annotation > `effects.envelopes:` entry
The two class-shaped strata distribute identically — an envelope keyed by a class name reaches every method key of THAT Ruby class, so a subclass's keys never match and a module distributes to its own methods only — and are applied in that order, so a written annotation always wins over a convention. Which config entry a class matched was already decided by ConfigEnvelopes.for_classes.
Public because LiskovCheck resolves the ancestor's envelope by exactly these rules: an inherited bound has to be the same bound the ancestor is itself held to, or the two checks would disagree about what the author wrote.
124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/rigor/effects/envelope_check.rb', line 124 def distribute(table, method_envelopes, class_envelopes, config_envelopes) resolved = {} unless class_envelopes.empty? && config_envelopes.empty? keys_by_class(table).each do |class_name, keys| envelope = class_envelopes[class_name] || config_envelopes[class_name] next if envelope.nil? keys.each { |key| resolved[key] = envelope.rebind(key) } end end method_envelopes.each { |key, envelope| resolved[key] = envelope if table[key] } resolved end |
.run(table:, method_envelopes:, class_envelopes:, config_envelopes: {}, positions: Positions.empty, apply_tolerated: true) ⇒ Array<Finding>
Returns sorted by position then key then label, so a run explains identically twice.
100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/rigor/effects/envelope_check.rb', line 100 def run(table:, method_envelopes:, class_envelopes:, config_envelopes: {}, positions: Positions.empty, apply_tolerated: true) envelopes = distribute(table, method_envelopes, class_envelopes, config_envelopes) return NO_FINDINGS if envelopes.empty? findings = [] envelopes.each do |key, envelope| collect(findings, table, key, envelope, positions, apply_tolerated) end findings.sort_by { |f| [f.path.to_s, f.line, f.key, f.label] }.freeze end |