Class: Rigor::Effects::SnapshotDiff

Inherits:
Object
  • Object
show all
Defined in:
lib/rigor/effects/snapshot_diff.rb

Overview

Compares a committed Snapshot with the one this run computes, and judges the difference (ADR-103 WD7; the event vocabulary is design note § 9.4).

The comparison itself is exact — drift between two observations by the same tool is 100 % precise. What is judged is whether the drift matters, and that has two knobs, applied here and never while writing the record:

  • the gate. symmetric (the default) fails on any drift, the db/schema.rb model: a removal is news too, because a job that stopped enqueueing is a bug rather than an improvement. additions is the ratchet: only growth fails.
  • the tolerated set. An event whose labels are all admitted by effects.tolerated: is reported under its own heading and does not fail the gate unless strict_tolerated: is set.

Additions are judged per origin; removals by label. The file is flat — origins are explain's job and a record keyed by them would churn on every refactor — but the current side of a comparison is a live effect table, so the caller can hand over undischarged:, the labels that survive per-origin discharge for each current symbol (#385). An added label is then tolerated exactly when every origin that introduces it is discharged, which is the same answer the envelope check gives, rather than the coarser "the label is on the list". A removal has no current-side origins to consult — the thing that produced it is gone — so it stays judged by label. Without the index (a caller that has no table, and every parse-only spec) both fall back to the label reading.

A header mismatch is neither: it is a regeneration event — the record was written by a different Rigor, a different vocabulary or a different effects: block, so the two sides are not comparable in the first place. It fails under both gates, because there is nothing to ratchet against.

Defined Under Namespace

Classes: Event

Constant Summary collapse

MISSING_SNAPSHOT =

Categories, in the order a text rendering emits them. The strings are the JSON contract.

"missing-snapshot"
REGENERATION =
"regeneration"
SYMBOL_ADDED =
"symbol-added"
SYMBOL_REMOVED =
"symbol-removed"
LABEL_ADDED =
"label-added"
LABEL_REMOVED =
"label-removed"
DECLARED_ADDED =
"declared-added"
DECLARED_REMOVED =
"declared-removed"
MATERIALISED =
"materialised"
EXHAUSTIVE_LOST =
"exhaustive-lost"
EXHAUSTIVE_GAINED =
"exhaustive-gained"
ADDITIVE_CATEGORIES =

What the additions ratchet still fails on: growth in either lane, a new symbol, and a method that stopped being exhaustive (someone introduced a call the analyzer cannot follow — the one "removal-shaped" change that is unambiguously a loss of knowledge).

[SYMBOL_ADDED, LABEL_ADDED, DECLARED_ADDED, EXHAUSTIVE_LOST,
MISSING_SNAPSHOT, REGENERATION].freeze
TABLES =
%w[methods reach].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(recorded:, current:, tolerated: [], gate: :symmetric, strict_tolerated: false, undischarged: nil) ⇒ SnapshotDiff

Returns a new instance of SnapshotDiff.



100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/rigor/effects/snapshot_diff.rb', line 100

def initialize(recorded:, current:, tolerated: [], gate: :symmetric, strict_tolerated: false,
               undischarged: nil)
  @recorded = recorded
  @current = current
  @gate = gate
  @strict_tolerated = strict_tolerated
  @tolerated = LabelSet.new(tolerated)
  @undischarged = undischarged
  @added_symbols = 0
  @removed_symbols = 0
  @events = build_events.freeze
  freeze
end

Instance Attribute Details

#currentObject (readonly)

Returns the value of attribute current.



98
99
100
# File 'lib/rigor/effects/snapshot_diff.rb', line 98

def current
  @current
end

#eventsObject (readonly)

Returns the value of attribute events.



98
99
100
# File 'lib/rigor/effects/snapshot_diff.rb', line 98

def events
  @events
end

#gateObject (readonly)

Returns the value of attribute gate.



98
99
100
# File 'lib/rigor/effects/snapshot_diff.rb', line 98

def gate
  @gate
end

#recordedObject (readonly)

Returns the value of attribute recorded.



98
99
100
# File 'lib/rigor/effects/snapshot_diff.rb', line 98

def recorded
  @recorded
end

Class Method Details

.compare(recorded:, current:, tolerated: [], gate: :symmetric, strict_tolerated: false, undischarged: nil) ⇒ Object

Compares current against recorded. recorded may be nil — no snapshot on disk at all, which is drift with a routed message rather than an error.

Parameters:

  • undischarged (Hash) (defaults to: nil)

    {table name => {symbol => [label]}} — the current side's labels that survive per-origin discharge (Rigor::Effects::Snapshot.undischarged_index). Optional; omitting it judges every event by label.



92
93
94
95
96
# File 'lib/rigor/effects/snapshot_diff.rb', line 92

def self.compare(recorded:, current:, tolerated: [], gate: :symmetric, strict_tolerated: false,
                 undischarged: nil)
  new(recorded: recorded, current: current, tolerated: tolerated, gate: gate,
      strict_tolerated: strict_tolerated, undischarged: undischarged)
end

Instance Method Details

#drift?Boolean

Whether rigor effects check exits non-zero.

Returns:

  • (Boolean)


121
122
123
# File 'lib/rigor/effects/snapshot_diff.rb', line 121

def drift?
  @events.any? { |event| gating?(event) }
end

#events_for(table) ⇒ Object



131
132
133
# File 'lib/rigor/effects/snapshot_diff.rb', line 131

def events_for(table)
  @events.select { |event| event.table == table && !event.tolerated? }
end

Renames are a removal plus an addition and are never reported as a lost effect; the footer is where the reviewer sees that the two counts balance.



127
128
129
# File 'lib/rigor/effects/snapshot_diff.rb', line 127

def footer
  { added_symbols: @added_symbols, removed_symbols: @removed_symbols }
end

#fresh?Boolean

Whether the record matches what this run computed. A tolerated-only difference is NOT fresh — the file still needs regenerating — it merely does not fail the gate.

Returns:

  • (Boolean)


116
117
118
# File 'lib/rigor/effects/snapshot_diff.rb', line 116

def fresh?
  @events.empty?
end

#tolerated_eventsObject



135
136
137
# File 'lib/rigor/effects/snapshot_diff.rb', line 135

def tolerated_events
  @events.select(&:tolerated?)
end