Class: Hecks::Runtime::DependencyPlanning::Analyzer

Inherits:
Object
  • Object
show all
Defined in:
lib/hecks/runtime/dependency_planning.rb

Constant Summary collapse

STATEFUL_MUTATIONS =
%i[append increment decrement multiply clamp remove].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(aggregate, command, root_aggregate = aggregate) ⇒ Analyzer

Returns a new instance of Analyzer.



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
108
109
110
111
112
113
114
115
116
# File 'lib/hecks/runtime/dependency_planning.rb', line 81

def initialize(aggregate, command, root_aggregate = aggregate)
  @aggregate = aggregate
  @command = command
  @owner_fields = aggregate.attributes.to_set(&:name)
  @owner_fields << aggregate.lifecycle.field.to_sym if aggregate.lifecycle
  @root_owner_fields = root_aggregate.attributes.to_set(&:name)
  @root_owner_fields << root_aggregate.lifecycle.field.to_sym if root_aggregate.lifecycle
  # `projects` FIELDS (S12, ADR 0025) ARE OWNER STATE TOO — a
  # `given`/`ensures` reading one (e.g. `customer_status ==
  # "active"`) is reading this record's own stored field, same
  # as any attribute, even though nothing here WRITES it via a
  # declared mutation (`CommandInterpreter#seed_projected_fields`
  # populates it outside this analysis entirely). Left out of
  # `known_writes` deliberately: `add_preservation_reads` then
  # correctly treats it as a prior-state read that must survive
  # a partial mutation, which is exactly right — a projected
  # field's freshness comes from the interpreter reseeding it on
  # save, not from anything a caller-supplied write set carries.
  # Applies to BOTH `owner_fields` and `root_owner_fields` — an
  # entity's own `parent.*` read can name the root aggregate's
  # projected field just as easily as one of its real attributes
  # (`Banking::Withdrawal.Dispute`'s own `parent.account_customer_
  # status`, ATMCard's projected field, is a real, live example).
  if aggregate.respond_to?(:projected_fields)
    aggregate.projected_fields.each { |field| @owner_fields << field.name }
  end
  if root_aggregate.respond_to?(:projected_fields)
    root_aggregate.projected_fields.each { |field| @root_owner_fields << field.name }
  end
  @payload_fields = command.attributes.to_set(&:name)
  @state_reads = Set.new
  @payload_reads = Set.new
  @writes = Set.new
  @known_writes = Set.new
  @unresolved = Set.new
end

Class Method Details

.call(aggregate:, command:, root_aggregate: aggregate) ⇒ Object

root_aggregate: — Wave 8's own audit surfaced a real bug here, not merely a missing feature: for an ENTITY-owned command, EntityInterpreter calls this with aggregate: set to the ENTITY itself (element_interpreter.rb's own Analyzer.call (aggregate: entity, command:)), so owner_fields was always the entity's own attribute set. A given/ensures reading parent.X legitimately means the ROOT aggregate's own field — a genuinely different owner — but classify_path's :parent branch checked that read against owner_fields (the entity's), which can never contain a root-level field, so every entity command with a real, legitimate parent.* read was refused as unresolved regardless of correctness. Defaults to aggregate (a no-op) for the plain-aggregate case — CommandInterpreter's own call site never needed to change.



79
# File 'lib/hecks/runtime/dependency_planning.rb', line 79

def self.call(aggregate:, command:, root_aggregate: aggregate) = new(aggregate, command, root_aggregate).call

Instance Method Details

#callObject



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/hecks/runtime/dependency_planning.rb', line 118

def call
  analyze_initial_state
  analyze_mutations
  analyze_lifecycle
  analyze_rules(command.givens, phase: :before)
  analyze_rules(command.ensures, phase: :after)
  analyze_rules(aggregate.invariants, phase: :after)
  add_preservation_reads

  complete = unresolved.empty? && owner_fields.subset?(known_writes)
  independent = complete && state_reads.empty?

  Plan.new(
    read_set:                sorted(state_reads),
    write_set:               sorted(writes),
    payload_read_set:        sorted(payload_reads),
    complete_state:          complete,
    state_independent:       independent,
    unresolved_dependencies: unresolved.to_a.sort.freeze
  ).freeze
end