Module: Hecks::Translation::Audit::LayerTwo
- Included in:
- Hecks::Translation::Audit
- Defined in:
- lib/hecks/translation/audit/layer_two.rb
Overview
Layer 2 — from the edge alone: per-rule value preservation, no leftover source keys, and id-set conservation across the edge.
Instance Method Summary collapse
-
#layer_two!(violations, aggregate, declared, before, after) ⇒ Object
Per-rule value preservation and leftover source keys are checked against the reference transform IN FULL — never rule by rule in isolation, because rules interact (a rename whose value a later move partially consumes preserves exactly what the transform says it preserves, no more).
- #normalize(state) ⇒ Object
Instance Method Details
#layer_two!(violations, aggregate, declared, before, after) ⇒ Object
Per-rule value preservation and leftover source keys are checked
against the reference transform IN FULL — never rule by rule in
isolation, because rules interact (a rename whose value a later
move partially consumes preserves exactly what the transform
says it preserves, no more). This makes every mint a run of the
cross-execution equivalence gate for the five portable rule
kinds: the compiled SQL produced after; the port's entry-JSON
transform produces expected; they must agree byte-for-byte on
every path a compute doesn't own. Compute paths are exempt — the
SQL is their only implementation, and the Layer-3 sample is
their only review. A rekeyed aggregate is exempt from this WHOLE
per-record check, for the same reason and one more: there is no
old-id → new-id correspondence to look after up by once the id
itself is what changed.
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/hecks/translation/audit/layer_two.rb', line 24 def layer_two!(violations, aggregate, declared, before, after) rekeyed = declared && !declared.rekeys.empty? # A rekey legitimately changes the id SET (that's the entire # point) — set-equality would flag every honest rekey as data # loss. What must still hold is RECORD COUNT: a botched rekey # colliding two distinct old ids onto one new id, or dropping one # (its SQL returning NULL), shows up as the count going down — # caught here without needing to track the old→new mapping # itself. if rekeyed unless before.keys.size == after.keys.size violations << "#{aggregate.name}: the record count changed across a rekeying edge " \ "(#{before.keys.size} before, #{after.keys.size} after) — a rekey must not " \ "collide two distinct ids onto one, or drop one" end elsif before.keys.sort != after.keys.sort gained = after.keys - before.keys lost = before.keys - after.keys violations << "#{aggregate.name}: the id set changed across the edge " \ "(lost #{lost.sort.inspect}, gained #{gained.sort.inspect})" end return unless declared # No correspondence between an old id and its rekeyed row exists # in Ruby (the rekey's SQL is its only implementation, same as # compute's) — exempt from the per-record equivalence gate for # the same reason compute paths already are, extended to the # whole record since an old-id lookup into `after` can never # succeed once the id itself has changed. return if rekeyed rules = Ports::Persistence::Lineage.from_declared(declared, aggregate.name) compute_tops = declared.computes.flat_map { |compute| [compute.from, compute.to] } .map { |path| path.to_s.split(".").first } before.each do |id, state| next unless after.key?(id) entry = Ports::Persistence::Entry.new(operation: "save", id: id, state: state.transform_keys(&:to_sym)) expected = normalize(rules.translate(entry).state).reject { |key, _| compute_tops.include?(key) } actual = normalize(after[id]).reject { |key, _| compute_tops.include?(key) } next if expected == actual diverged = (expected.keys | actual.keys).select { |key| expected[key] != actual[key] } violations << "#{aggregate.name}##{id}: the translated state diverges from the reference " \ "transform at #{diverged.sort.join(', ')}" end end |
#normalize(state) ⇒ Object
73 |
# File 'lib/hecks/translation/audit/layer_two.rb', line 73 def normalize(state) = JSON.parse(JSON.generate(state)) |