Class: Hecks::Ports::Persistence::Lineage
- Inherits:
-
Object
- Object
- Hecks::Ports::Persistence::Lineage
- Defined in:
- lib/hecks/ports/persistence/lineage.rb
Overview
Translates a journal entry written under an old shape into the one the current bluebook declares — a renamed attribute, a renamed aggregate, or a field crossing a value-object boundary. Applied wherever entries are read, so replay derives the current head from history that was never rewritten — the port owns the meaning, adapters stay oblivious.
Instance Attribute Summary collapse
-
#ancestor_name ⇒ Object
readonly
ancestor_nameis the declared name a rename came from (matches the held bluebook's own aggregate names);ancestor_storage_nameis its derived, snake_case file/table name. -
#ancestor_storage_name ⇒ Object
readonly
ancestor_nameis the declared name a rename came from (matches the held bluebook's own aggregate names);ancestor_storage_nameis its derived, snake_case file/table name.
Class Method Summary collapse
- .for(registry, domain, aggregate) ⇒ Object
-
.from_declared(declared, aggregate_name) ⇒ Object
One SPECIFIC edge's rules for one aggregate — what the mint path uses, where
forwould happily answer with whichever edge in the registry mentioned the aggregate first.
Instance Method Summary collapse
-
#computes? ⇒ Boolean
Whether any rule in this edge is a compute — the one rule kind with no in-process implementation at all.
-
#explains?(path) ⇒ Boolean
Whether this translation names
pathas an old key it accounts for — the rename, move, or convert it came from, or an explicit drop. -
#fills?(path) ⇒ Boolean
THE DESTINATION-SIDE TWIN of
explains?above, which only ever asks about a rule's SOURCE. -
#initialize(renames, moves = [], converts = [], drops = [], retypes: [], computes: [], rekeys: [], backfills: [], ancestor_name: nil, ancestor_storage_name: nil) ⇒ Lineage
constructor
A new instance of Lineage.
-
#rekey? ⇒ Boolean
THE SINGLE SOURCE OF TRUTH for "does this edge rekey this aggregate" — every consumer (coverage_check.rb's identity gate, minter.rb's approval gate, layer_two.rb's audit, head_compiler.rb's SQL compilation) asks THIS, never re-derives it from
declaredindependently. -
#rekey_sql ⇒ Object
The rekey's own SQL — first-and-only rule, same one-per-aggregate assumption
computemakes about its own list where it matters (an edge with more than one is a DSL-level decision, not something this reader arbitrates). -
#retype?(held_type, current_type) ⇒ Boolean
Whether a declared retype says the pair of TYPE names means the same shape — a value object or entity whose own name changed with its members intact.
-
#translate(entry) ⇒ Object
The reference semantics for the five PORTABLE rule kinds — rename, move, convert, drop, and the aggregate-level
was:.
Constructor Details
#initialize(renames, moves = [], converts = [], drops = [], retypes: [], computes: [], rekeys: [], backfills: [], ancestor_name: nil, ancestor_storage_name: nil) ⇒ Lineage
Returns a new instance of Lineage.
46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/hecks/ports/persistence/lineage.rb', line 46 def initialize(renames, moves = [], converts = [], drops = [], retypes: [], computes: [], rekeys: [], backfills: [], ancestor_name: nil, ancestor_storage_name: nil) @renames = renames @moves = moves @converts = converts @drops = drops @retypes = retypes @computes = computes @rekeys = rekeys @backfills = backfills @ancestor_name = ancestor_name @ancestor_storage_name = ancestor_storage_name end |
Instance Attribute Details
#ancestor_name ⇒ Object (readonly)
ancestor_name is the declared name a rename came from (matches
the held bluebook's own aggregate names); ancestor_storage_name
is its derived, snake_case file/table name. Both are nil unless
the aggregate itself was renamed.
19 20 21 |
# File 'lib/hecks/ports/persistence/lineage.rb', line 19 def ancestor_name @ancestor_name end |
#ancestor_storage_name ⇒ Object (readonly)
ancestor_name is the declared name a rename came from (matches
the held bluebook's own aggregate names); ancestor_storage_name
is its derived, snake_case file/table name. Both are nil unless
the aggregate itself was renamed.
19 20 21 |
# File 'lib/hecks/ports/persistence/lineage.rb', line 19 def ancestor_storage_name @ancestor_storage_name end |
Class Method Details
.for(registry, domain, aggregate) ⇒ Object
21 22 23 24 25 26 27 28 |
# File 'lib/hecks/ports/persistence/lineage.rb', line 21 def self.for(registry, domain, aggregate) translation = registry.translations.find do |candidate| candidate.domain == domain.to_s && candidate.for_aggregate(aggregate.name) end return nil unless translation from_declared(translation.for_aggregate(aggregate.name), aggregate.name) end |
.from_declared(declared, aggregate_name) ⇒ Object
One SPECIFIC edge's rules for one aggregate — what the mint
path uses, where for would happily answer with whichever
edge in the registry mentioned the aggregate first.
33 34 35 36 37 38 39 40 41 42 43 44 |
# File 'lib/hecks/ports/persistence/lineage.rb', line 33 def self.from_declared(declared, aggregate_name) return nil unless declared renamed_aggregate = declared.was && declared.was != aggregate_name.to_s ancestor_name = renamed_aggregate ? declared.was : nil ancestor_storage_name = renamed_aggregate ? Naming.snake(declared.was) : nil new(declared.renames, declared.moves, declared.converts, declared.drops, retypes: declared.retypes, computes: declared.computes, rekeys: declared.rekeys, backfills: declared.backfills, ancestor_name: ancestor_name, ancestor_storage_name: ancestor_storage_name) end |
Instance Method Details
#computes? ⇒ Boolean
Whether any rule in this edge is a compute — the one rule kind with no in-process implementation at all. An aggregate carrying one refuses to boot anywhere but Postgres, per-rule and by name, before the general drift machinery says anything vaguer.
64 |
# File 'lib/hecks/ports/persistence/lineage.rb', line 64 def computes? = !@computes.empty? |
#explains?(path) ⇒ Boolean
Whether this translation names path as an old key it accounts
for — the rename, move, or convert it came from, or an explicit
drop. path is a bare name ("cost") or a dotted value-object
member ("price.currency"); a rule covering the WHOLE top-level
attribute (a rename, a top-level move/convert/drop) also covers
anything nested under it, since the whole value travels or goes
away together. Used to catch a field — or a value object's own
member — that vanished (or silently changed type) without
anything explaining it, even when some OTHER field is covered.
backfills matches on the WHOLE name only, never a dotted
prefix — a backfill names a top-level attribute that is new
outright (nothing to be a prefix of on the held side), unlike
every rule above it, which explains a path that existed and
moved, converted, or vanished.
120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/hecks/ports/persistence/lineage.rb', line 120 def explains?(path) path = path.to_s top = path.split(".").first @renames.key?(top.to_sym) || @moves.any? { |move| move.from == path || move.from.split(".").first == top } || @converts.any? { |convert| convert.from == path || convert.from.split(".").first == top } || @drops.any? { |drop| drop.to_s == path || drop.to_s.split(".").first == top } || @computes.any? { |compute| compute.from == path || compute.from.split(".").first == top } || @backfills.any? { |backfill| backfill.name.to_s == top } end |
#fills?(path) ⇒ Boolean
THE DESTINATION-SIDE TWIN of explains? above, which only ever
asks about a rule's SOURCE. unsafe_additions asks a different
question — not "was this vanished path accounted for" but "does
an existing record end up with a value here" — and a move or
convert whose to: lands a old field inside a BRAND-NEW
top-level attribute (weight becoming contents.weight when
Contents did not exist before) fills that attribute for an
existing record exactly as a backfill would, even though
nothing named contents explains any vanished path. compute
counts on the same terms explains? already grants it
elsewhere in this file — Postgres-only and audited, not
actually applied by THIS method, the same gap the vanish side
already lives with.
@renames.value? belongs here too, and used to be missing: a
bare rename :cost, to: :amount is the plainest possible
covering rule there is (translate above applies it
unconditionally, no lookup table, no per-record ambiguity —
simpler than a move or convert, which both got their fills?
entry from the start), and its absence meant unsafe_additions
reported the new name as an unexplained required addition on
EVERY rename-only edge, the single most common translation
shape there is. explains? already checked the source side
(@renames.key?); fills? is the symmetric destination-side
check that was never added alongside it.
157 158 159 160 161 162 163 164 165 |
# File 'lib/hecks/ports/persistence/lineage.rb', line 157 def fills?(path) path = path.to_s @renames.value?(path.to_sym) || @moves.any? { |move| move.to.split(".").first == path } || @converts.any? { |convert| convert.to.split(".").first == path } || @computes.any? { |compute| compute.to.split(".").first == path } || @backfills.any? { |backfill| backfill.name.to_s == path } end |
#rekey? ⇒ Boolean
THE SINGLE SOURCE OF TRUTH for "does this edge rekey this
aggregate" — every consumer (coverage_check.rb's identity gate,
minter.rb's approval gate, layer_two.rb's audit, head_compiler.rb's
SQL compilation) asks THIS, never re-derives it from declared
independently. One accessor to change if what a rekey rule means
ever needs to change, not four call sites in four files.
72 |
# File 'lib/hecks/ports/persistence/lineage.rb', line 72 def rekey? = !@rekeys.empty? |
#rekey_sql ⇒ Object
The rekey's own SQL — first-and-only rule, same one-per-aggregate
assumption compute makes about its own list where it matters
(an edge with more than one is a DSL-level decision, not
something this reader arbitrates).
78 |
# File 'lib/hecks/ports/persistence/lineage.rb', line 78 def rekey_sql = @rekeys.first&.sql |
#retype?(held_type, current_type) ⇒ Boolean
Whether a declared retype says the pair of TYPE names means the same shape — a value object or entity whose own name changed with its members intact. Nothing in the stored data carries the type name, so this never moves a value; it only satisfies the era diff's literal type-name comparison.
172 173 174 |
# File 'lib/hecks/ports/persistence/lineage.rb', line 172 def retype?(held_type, current_type) @retypes.any? { |retype| retype.from == held_type.to_s && retype.to == current_type.to_s } end |
#translate(entry) ⇒ Object
The reference semantics for the five PORTABLE rule kinds —
rename, move, convert, drop, and the aggregate-level was:.
retype moves nothing (stored state never carries a type name)
and compute is deliberately not applied here: its SQL is its
only implementation, so this transform neither imitates nor
checks it — the source field passes through untouched, and the
audit verifies compute output against the matview alone.
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/hecks/ports/persistence/lineage.rb', line 87 def translate(entry) return entry unless entry.save? && entry.state # Deep, not shallow: a move or convert reaches INTO a nested # value-object hash, and a shallow dup would quietly mutate # the caller's copy of the original entry. state = deep_dup(entry.state) @renames.each { |old_name, new_name| state[new_name] = state.delete(old_name) if state.key?(old_name) } @moves.each { |move| apply_move(state, move) } @converts.each { |convert| apply_convert(state, convert) } @drops.each { |name| apply_drop(state, name) } # LAST, and only where nothing already answered — a backfill # fills the gap a rename/move/convert left untouched, never # overwrites a value that already made it across. @backfills.each { |backfill| state[backfill.name] = backfill.default unless state.key?(backfill.name) } Entry.new(operation: entry.operation, id: entry.id, state: state, mirrors: entry.mirrors) end |