Module: Hecks::Translation::Scaffold::Differ
- Included in:
- Hecks::Translation::Scaffold
- Defined in:
- lib/hecks/translation/scaffold/differ.rb
Overview
Diff two eras' storage-shape projections into the edge's declarations — confident rules by unique signature pairing, everything ambiguous left unresolved for a human.
Instance Method Summary collapse
- #appeared_paths(held_attrs, current_attrs, retyped) ⇒ Object
-
#attribute_rules(held_shape, current_shape) ⇒ Object
Every changed path in one aggregate, resolved into rules by signature matching over the FULL path set — top-level names and dotted members alike.
- #compatible_candidates(appeared, signature) ⇒ Object
- #container?(attribute) ⇒ Boolean
-
#diff(held_bluebook, current_bluebook) ⇒ Object
Diff two bluebook IRs into the edge's declarations.
-
#identity_hint(held_shape, current_shape) ⇒ Object
THE ONE THING
attribute_rulesCOULD NOT SEE BEFORE — the shape projection already carries"identity"(StorageShape .project_aggregate), it was simply never read here. -
#match_aggregates(held_shapes, current_shapes) ⇒ Object
A vanished aggregate whose full shape reappears under exactly one new name was renamed.
- #members_of(attribute) ⇒ Object
- #projections(bluebook) ⇒ Object
- #scalar_of(signature) ⇒ Object
-
#vanished_paths(held_attrs, current_attrs, retyped) ⇒ Object
Held paths that need explaining: whole attributes that vanished, and members that vanished (or changed type) inside a kept attribute — mirroring exactly what EraGuard will demand coverage for.
Instance Method Details
#appeared_paths(held_attrs, current_attrs, retyped) ⇒ Object
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/hecks/translation/scaffold/differ.rb', line 134 def appeared_paths(held_attrs, current_attrs, retyped) paths = {} current_attrs.each do |name, current| next if retyped.include?(container?(current) ? current["type"]["type"] : nil) held = held_attrs[name] if held.nil? paths[name] = current["type"] next end next if held == current held_members = members_of(held) members_of(current).each do |member, signature| paths["#{name}.#{member}"] = signature if held_members[member] != signature end end paths end |
#attribute_rules(held_shape, current_shape) ⇒ Object
Every changed path in one aggregate, resolved into rules by signature matching over the FULL path set — top-level names and dotted members alike. Unique signature pair: a rename (both top-level) or a move (any dotted end). Same path, same members, new type name: a retype. Anything else: unresolved, carrying its type-compatible candidates (an empty list is the arrow toward compute, or drop).
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/hecks/translation/scaffold/differ.rb', line 70 def attribute_rules(held_shape, current_shape) rules = [] rules << identity_hint(held_shape, current_shape) rules.compact! held_attrs = (held_shape["attributes"] || []).to_h { |attribute| [attribute["name"], attribute] } current_attrs = (current_shape["attributes"] || []).to_h { |attribute| [attribute["name"], attribute] } # retype pass: same attribute name, same member structure, the # TYPE's own name changed (held_attrs.keys & current_attrs.keys).each do |name| held = held_attrs[name] current = current_attrs[name] next if held == current next unless container?(held) && container?(current) next unless held["type"]["members"] == current["type"]["members"] && held["list"] == current["list"] rules << { kind: :retype, from: held["type"]["type"], to: current["type"]["type"] } end retyped = rules.filter_map { |rule| rule[:from] if rule[:kind] == :retype } vanished = vanished_paths(held_attrs, current_attrs, retyped) appeared = appeared_paths(held_attrs, current_attrs, retyped) vanished.each do |path, signature| matches = appeared.select { |_, candidate| candidate == signature }.keys taken = rules.filter_map { |rule| rule[:to] if %i[rename move].include?(rule[:kind]) } matches -= taken if matches.size == 1 && vanished.count { |_, other| other == signature } == 1 target = matches.first kind = path.include?(".") || target.include?(".") ? :move : :rename rules << { kind: kind, from: path, to: target } else candidates = matches.empty? ? compatible_candidates(appeared, signature) : matches rules << { kind: :unresolved, from: path, candidates: candidates } end end rules end |
#compatible_candidates(appeared, signature) ⇒ Object
167 168 169 |
# File 'lib/hecks/translation/scaffold/differ.rb', line 167 def compatible_candidates(appeared, signature) appeared.select { |_, candidate| scalar_of(candidate) == scalar_of(signature) }.keys end |
#container?(attribute) ⇒ Boolean
179 |
# File 'lib/hecks/translation/scaffold/differ.rb', line 179 def container?(attribute) = attribute["type"].is_a?(Hash) |
#diff(held_bluebook, current_bluebook) ⇒ Object
Diff two bluebook IRs into the edge's declarations.
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# File 'lib/hecks/translation/scaffold/differ.rb', line 11 def diff(held_bluebook, current_bluebook) held_shapes = projections(held_bluebook) current_shapes = projections(current_bluebook) matched = match_aggregates(held_shapes, current_shapes) aggregates = current_bluebook.aggregates.filter_map do |aggregate| held_name = matched[:pairs][aggregate.name] next unless held_name rules = attribute_rules(held_shapes[held_name], current_shapes[aggregate.name]) was = held_name == aggregate.name ? nil : held_name next if rules.empty? && was.nil? ScaffoldedAggregate.new(name: aggregate.name, was: was, rules: rules) end { aggregates: aggregates, retired: matched[:retired], unclaimed: matched[:unclaimed] } end |
#identity_hint(held_shape, current_shape) ⇒ Object
THE ONE THING attribute_rules COULD NOT SEE BEFORE — the shape
projection already carries "identity" (StorageShape .project_aggregate), it was simply never read here. An
unresolved placeholder, not a guess: coverage_check.rb's own
check_identity_unchanged! is the real gate this only hints
toward, the same "tool proactively guides you" pattern the
generic unresolved message already gives unfed fields.
161 162 163 164 165 |
# File 'lib/hecks/translation/scaffold/differ.rb', line 161 def identity_hint(held_shape, current_shape) return if held_shape["identity"] == current_shape["identity"] { kind: :unresolved, from: :identity, candidates: [] } end |
#match_aggregates(held_shapes, current_shapes) ⇒ Object
A vanished aggregate whose full shape reappears under exactly one
new name was renamed. retired is only confident when NOTHING
remains it could plausibly have become — a vanished aggregate
beside an unmatched new one might be a rename-plus-reshape, and
writing retired there would be a guess that strands data.
Anything ambiguous stays unclaimed, and the coverage gate names
it until a human decides.
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/hecks/translation/scaffold/differ.rb', line 41 def match_aggregates(held_shapes, current_shapes) pairs = {} current_shapes.each_key { |name| pairs[name] = held_shapes.key?(name) ? name : nil } vanished = held_shapes.keys - current_shapes.keys appeared = current_shapes.keys.select { |name| pairs[name].nil? } unclaimed = [] vanished.each do |old_name| old_shape = held_shapes[old_name].merge("name" => nil) candidates = appeared.select { |name| current_shapes[name].merge("name" => nil) == old_shape } if candidates.size == 1 pairs[candidates.first] = old_name appeared -= candidates else unclaimed << old_name end end retired = appeared.empty? ? unclaimed : [] { pairs: pairs, retired: retired, unclaimed: retired.empty? ? unclaimed : [] } end |
#members_of(attribute) ⇒ Object
173 174 175 176 177 |
# File 'lib/hecks/translation/scaffold/differ.rb', line 173 def members_of(attribute) return {} unless container?(attribute) attribute["type"]["members"].to_h { |member| [member["name"], member["type"]] } end |
#projections(bluebook) ⇒ Object
30 31 32 |
# File 'lib/hecks/translation/scaffold/differ.rb', line 30 def projections(bluebook) Runtime::StorageShape.project(bluebook)["aggregates"].to_h { |shape| [shape["name"], shape] } end |
#scalar_of(signature) ⇒ Object
171 |
# File 'lib/hecks/translation/scaffold/differ.rb', line 171 def scalar_of(signature) = signature.is_a?(Hash) ? signature["members"] : signature |
#vanished_paths(held_attrs, current_attrs, retyped) ⇒ Object
Held paths that need explaining: whole attributes that vanished, and members that vanished (or changed type) inside a kept attribute — mirroring exactly what EraGuard will demand coverage for.
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/hecks/translation/scaffold/differ.rb', line 113 def vanished_paths(held_attrs, current_attrs, retyped) paths = {} held_attrs.each do |name, held| next if retyped.include?(container?(held) ? held["type"]["type"] : nil) current = current_attrs[name] if current.nil? paths[name] = held["type"] next end next if held == current held_members = members_of(held) current_members = members_of(current) held_members.each do |member, signature| paths["#{name}.#{member}"] = signature if current_members[member] != signature end end paths end |