Class: Rigor::Effects::Snapshot
- Inherits:
-
Object
- Object
- Rigor::Effects::Snapshot
- Defined in:
- lib/rigor/effects/snapshot.rb
Overview
The committed effect snapshot — .rigor-effects.yml, the primary validation mode (ADR-103 WD7 /
WD14, design note § 9.4).
It is db/schema.rb for effects: a generated artefact a project commits, whose diff in a pull
request is the review signal, whose freshness CI checks, and where intent is expressed by
committing the regenerated file rather than by annotating code. It emits no diagnostic and never
enters rigor check's stream (ADR-102).
Two tables, chosen so a diff is attributable:
methods:holds each unit's direct summary — origins in the method's own body (block literals included) plus catalogued and attributed callees, but never a project callee, which is an edge. An entry therefore moves only when its own lines, the catalogue or an attribution moved.reach:holds the transitive footprint at the entry pointseffects.snapshot.reach:names. A leaf change fans out here, and the fan-out is the information (blast radius).
The record is undischarged: tolerated: is consulted at judgment time by SnapshotDiff, never
while writing, so update --no-tolerated-effects and update write byte-identical files and a
policy change diffs the config rather than the record.
The serialisation is a hand-rolled JSON-compatible YAML subset: string keys, JSON scalars, flow
sequences, sorted keys and sorted labels, no anchors, no tags, no timestamps. YAML.safe_load of the
file round-trips through JSON unchanged, which is the property a sibling implementation reads it by.
Defined Under Namespace
Classes: Entry, ParseError
Constant Summary collapse
- SCHEMA =
Bumped when the file's shape changes in a way an older reader would misread. A bump makes every existing file a regeneration event rather than a silent reinterpretation.
1- HEADER =
"# .rigor-effects.yml — generated by `rigor effects update`. Commit it; review its diff."
Instance Attribute Summary collapse
-
#header ⇒ Object
readonly
Returns the value of attribute header.
-
#methods ⇒ Object
readonly
Returns the value of attribute methods.
-
#reach ⇒ Object
readonly
Returns the value of attribute reach.
Class Method Summary collapse
-
.build(table:, configuration:, sources: {}, full: false, registry: Registry.default, project_root: Dir.pwd) ⇒ Object
Builds the snapshot a run's effect table describes.
-
.config_digest(configuration) ⇒ Object
The
effects:block of.rigor.yml, canonicalised (keys sorted at every depth, rendered as JSON) and hashed. -
.expand_reach(entries) ⇒ Object
Resolves
effects.snapshot.reach:entries to file globs: a preset name becomes the globs it stands for, a glob stays itself. - .load(path) ⇒ Object
- .parse(text) ⇒ Object
-
.undischarged_index(snapshot:, table:, discharge:) ⇒ Hash?
The per-origin discharge judgment's input, for the side of a comparison that HAS origins (#385).
Instance Method Summary collapse
- #==(other) ⇒ Object (also: #eql?)
- #hash ⇒ Object
-
#initialize(header:, methods: {}, reach: {}) ⇒ Snapshot
constructor
A new instance of Snapshot.
-
#table(name) ⇒ Object
The table a diff category names, so
SnapshotDiffwalks the two by name. -
#to_h ⇒ Object
The wire form: plain Hashes, Strings, Integers, Booleans and Arrays — nothing a JSON parser would choke on and nothing a YAML reader has to resolve a tag for.
- #to_yaml ⇒ Object (also: #to_s)
Constructor Details
#initialize(header:, methods: {}, reach: {}) ⇒ Snapshot
Returns a new instance of Snapshot.
301 302 303 304 305 306 |
# File 'lib/rigor/effects/snapshot.rb', line 301 def initialize(header:, methods: {}, reach: {}) @header = header.freeze @methods = sorted(methods) @reach = sorted(reach) freeze end |
Instance Attribute Details
#header ⇒ Object (readonly)
Returns the value of attribute header.
299 300 301 |
# File 'lib/rigor/effects/snapshot.rb', line 299 def header @header end |
#methods ⇒ Object (readonly)
Returns the value of attribute methods.
299 300 301 |
# File 'lib/rigor/effects/snapshot.rb', line 299 def methods @methods end |
#reach ⇒ Object (readonly)
Returns the value of attribute reach.
299 300 301 |
# File 'lib/rigor/effects/snapshot.rb', line 299 def reach @reach end |
Class Method Details
.build(table:, configuration:, sources: {}, full: false, registry: Registry.default, project_root: Dir.pwd) ⇒ Object
Builds the snapshot a run's effect table describes.
102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/rigor/effects/snapshot.rb', line 102 def build(table:, configuration:, sources: {}, full: false, registry: Registry.default, project_root: Dir.pwd) globs = (configuration.effects_snapshot_reach) new( header: { "schema" => SCHEMA, "rigor" => Rigor::VERSION, "vocabulary" => registry.vocabulary_version, "config_digest" => config_digest(configuration) }, methods: build_methods(table, full: full), reach: build_reach(table, globs, sources, project_root, full: full) ) end |
.config_digest(configuration) ⇒ Object
The effects: block of .rigor.yml, canonicalised (keys sorted at every depth, rendered as
JSON) and hashed. A Rigor upgrade, a vocabulary bump or a tolerated: edit is therefore a
visible regeneration event rather than a silent reinterpretation of the record.
The implementation lives in Identity because the effects CACHE identity (#382) digests the same block: a header that agreed with the cache key only by convention would eventually stop agreeing, and the failure would be a silently reused stale sidecar.
151 152 153 |
# File 'lib/rigor/effects/snapshot.rb', line 151 def config_digest(configuration) Identity.config_digest(configuration) end |
.expand_reach(entries) ⇒ Object
Resolves effects.snapshot.reach: entries to file globs: a preset name becomes the globs it
stands for, a glob stays itself.
This is where an unknown preset name is caught (#387). Configuration cannot: presets are named
by plugins, and plugins load from the configuration being validated. By the time a snapshot is
built the registered set is complete, so a name nothing registered is a real error — most often
reach: [rails] in a project that never listed the Rails plugins — and saying so beats a reach:
table that comes back mysteriously empty.
163 164 165 166 167 168 169 |
# File 'lib/rigor/effects/snapshot.rb', line 163 def (entries) entries.flat_map do |entry| next [entry] if EntryPoints.glob?(entry) EntryPoints.resolve!(entry) end.uniq.sort.freeze end |
.load(path) ⇒ Object
184 185 186 |
# File 'lib/rigor/effects/snapshot.rb', line 184 def load(path) parse(File.read(path, encoding: "UTF-8")) end |
.parse(text) ⇒ Object
171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/rigor/effects/snapshot.rb', line 171 def parse(text) data = YAML.safe_load(text) raise ParseError, "not a mapping" unless data.is_a?(Hash) new( header: parse_header(data), methods: parse_table(data["methods"], "methods"), reach: parse_table(data["reach"], "reach") ) rescue Psych::Exception => e raise ParseError, e. end |
.undischarged_index(snapshot:, table:, discharge:) ⇒ Hash?
The per-origin discharge judgment's input, for the side of a comparison that HAS origins
(#385). {table => {symbol => [label]}}: for each row this snapshot carries, the labels that
survive effects.tolerated: applied bundle by bundle — direct bundles under methods:, the
transitive lane under reach:, matching what each table records.
It is built beside the snapshot and never inside it: the file stays flat and undischarged, and this index exists only for the duration of one comparison (Rigor::Effects::SnapshotDiff).
129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
# File 'lib/rigor/effects/snapshot.rb', line 129 def undischarged_index(snapshot:, table:, discharge:) return nil if discharge.inert? { "methods" => snapshot.methods.keys.filter_map do |key| entry = table[key] [key, discharge.undischarged(entry.direct.bundles).to_a] if entry end.to_h, "reach" => snapshot.reach.keys.filter_map do |key| entry = table[key] [key, entry.undischarged.to_a] if entry end.to_h }.freeze end |
Instance Method Details
#==(other) ⇒ Object Also known as: eql?
308 309 310 |
# File 'lib/rigor/effects/snapshot.rb', line 308 def ==(other) other.is_a?(Snapshot) && other.header == @header && other.methods == @methods && other.reach == @reach end |
#hash ⇒ Object
313 314 315 |
# File 'lib/rigor/effects/snapshot.rb', line 313 def hash [self.class, @header, @methods, @reach].hash end |
#table(name) ⇒ Object
The table a diff category names, so SnapshotDiff walks the two by name.
318 319 320 |
# File 'lib/rigor/effects/snapshot.rb', line 318 def table(name) name == "reach" ? @reach : @methods end |
#to_h ⇒ Object
The wire form: plain Hashes, Strings, Integers, Booleans and Arrays — nothing a JSON parser would choke on and nothing a YAML reader has to resolve a tag for.
324 325 326 327 328 329 |
# File 'lib/rigor/effects/snapshot.rb', line 324 def to_h @header.merge( "methods" => @methods.transform_values(&:to_h), "reach" => @reach.transform_values(&:to_h) ) end |
#to_yaml ⇒ Object Also known as: to_s
353 354 355 356 357 358 359 360 361 362 363 |
# File 'lib/rigor/effects/snapshot.rb', line 353 def to_yaml lines = [HEADER] lines << "schema: #{@header.fetch('schema')}" lines << "rigor: #{scalar(@header.fetch('rigor'))}" lines << "vocabulary: #{@header.fetch('vocabulary')}" lines << "config_digest: #{scalar(@header.fetch('config_digest'))}" render_table(lines, "methods", @methods) lines.concat(EMPTY_REACH_NOTE) if @reach.empty? render_table(lines, "reach", @reach) "#{lines.join("\n")}\n" end |