Class: Hecks::Bluebook::DSL::BluebookBuilder

Inherits:
Object
  • Object
show all
Includes:
WordGate
Defined in:
lib/hecks/bluebook/dsl/bluebook_builder.rb

Constant Summary collapse

GRAMMAR_CONTEXT =
"Bluebook"

Constants included from WordGate

WordGate::NOT_ADMITTED

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, version: nil) ⇒ BluebookBuilder

Returns a new instance of BluebookBuilder.



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 12

def initialize(name, version: nil)
  @name       = name
  @version    = version
  @aggregates       = []
  @read_models      = []
  @policies         = []
  @process_managers = []
  # THE ROOT of the CHAPTER-WIDE given pool — one level wider
  # than `AggregateBuilder`'s own `@entity_named_givens` (S10
  # extended across an aggregate's whole entity tree, earlier
  # this arc). See `#given`'s own comment for what this closes.
  @chapter_named_givens = {}
  # EVERY BARE CHAPTER-GIVEN REFERENCE THIS CHAPTER'S OWN FILES
  # LEFT UNRESOLVED SO FAR — threaded into every aggregate the
  # same way `@chapter_named_givens` is. See
  # `AggregateBuilder#pending_chapter_given`'s own comment for
  # what queues here and `#resolve_pending_chapter_givens!`,
  # below, for where it drains.
  @chapter_pending_givens = []
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method in the class Hecks::Bluebook::DSL::WordGate

Instance Attribute Details

#classificationObject (readonly)

Returns the value of attribute classification.



10
11
12
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 10

def classification
  @classification
end

Class Method Details

.build(name, version: nil, &block) ⇒ Object

A CHAPTER MAY BE DECLARED IN SEVERAL FILES, meant to merge into ONE domain — lib/hecks/language/bluebook/*.bluebook all open Hecks.bluebook "Bluebook" do ... end. Each Hecks.bluebook call used to mint a fresh builder, so a second file with the same chapter name silently replaced the first's aggregates instead of adding to them.

The registry now holds the builder OPEN across calls : the first file for a name creates it, every later file for the same name reuses the same instance, so @aggregates/@read_models accumulate. #build is safe to call once per file on the same builder — it constructs a fresh Bluebook from whatever is currently held and re-Namespace.installs over the previous one, so the LAST file's call leaves every aggregate seen so far reachable, and each call's IR is a strict superset of the one before. Registry#add_bluebook still simply stores by name — with this in place, "last write wins" is the cumulative, correct write.



997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 997

def self.build(name, version: nil, &block)
  registry = Hecks.current_registry
  builder  = registry ? registry.bluebook_builder(name) { new(name, version: version) } : new(name, version: version)
  builder.__send__(:adopt_version, version)
  # A bare constant in a bluebook — `attribute :name, PizzaName` — is a NAME,
  # not a reference to something Ruby has heard of. `const_missing` hands
  # over a `ConstShim::ScopedConstant` (S0b, const_shim.rb's own comment),
  # and that is still the whole answer for a bare name: `Attribute` spells
  # it with `to_s`, so the `TypeName` wrapper this used to build existed
  # only long enough to be stringified. The concept still has a home — the
  # language declares `value_object "TypeName"` — it just needed no Ruby
  # class of its own. A Module rather than a Symbol is what also lets
  # `Account::Debit`/`admits: Account::LedgerDirection` answer their OWN
  # `::` — a plain Symbol cannot.
  resolver = ->(const) { ConstShim::ScopedConstant.for(const) }
  ConstShim.with(resolver) { builder.instance_eval(&block) } if block
  builder.build
end

.check_with_spec!(command_ref, event_name, with_spec, lookup, label, aggregates, correlation_heads, pm: nil) ⇒ Object

pm: is present only for a process manager's own dispatch — a saga leg's source symbol resolves against the CURRENT triggering event first, same as a policy, but falls all the way back to the saga's own MEMORY when the current event does not carry it (SagaInterpreter#dispatch_args, its own last else) — and memory starts as the OPENING event's payload (SagaInterpreter#instance = { ..., memory: event.payload }, never updated after), never the leg's own. Settlement's own comment names exactly this: "the credit leg reads a destination no event carried" — AccountDebited never declares :reference, only TransferRequested (pm.starts_on) does, and that is where the value is genuinely still coming from.



396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 396

def self.check_with_spec!(command_ref, event_name, with_spec, lookup, label, aggregates, correlation_heads, pm: nil)
  target        = lookup[command_ref]
  source_shape  = event_name && event_shape_for(event_name, aggregates)
  memory_shape  = pm && event_shape_for(pm.starts_on, aggregates)
  correlation   = pm && pm.correlates_by && pm.correlation_head
  # A POLICY'S SOURCE ALSO CARRIES THE EMITTER'S OWN IDENTITY —
  # `PolicyInterpreter#emitter_identity`, the runtime half of this.
  # An entity command's event never declares its aggregate's
  # identity (it arrives through `reference_to`, not an
  # `attribute`), so before this a policy on `KnightCaptured` could
  # not spell `with: { label: :label }` at all — "reads :label off
  # KnightCaptured, which does not declare it" — and chess's
  # AdvancePly grew optional, unread attributes just to survive a
  # wholesale forward. Policies only: a saga leg's own source is
  # `SagaInterpreter#dispatch_args`, which merges no such thing.
  identity_sources = pm.nil? && event_name ? event_identity_heads_for(event_name, aggregates) : []

  with_spec.each do |field, source|
    raise Malformed, "#{label}'s with: names #{field.inspect}, which #{command_ref} does not declare" if target && !command_declares?(target, field, aggregates, correlation_heads)

    next unless source.is_a?(::Symbol)
    next if source == correlation
    next if identity_sources.include?(source)
    next unless source_shape || memory_shape

    found = [source_shape, memory_shape].compact.any? { |shape| shape.any? { |name, *| name == source } }
    next if found

    raise Malformed, "#{label}'s with: reads :#{source} off #{event_name.inspect}, which does not declare it"
  end
end

.command_declares?(command, field, aggregates, correlation_heads) ⇒ Boolean

A command's OWN reference_to (bare, no as:) never lands in attributesCommandBuilder#reference_to's self-reference branch sets command.references instead (S2), and mints no new field at all. What addresses it is not one name but the SAME SET CommandInterpreter::ArgumentGate#refuse_unknown_arguments already accepts at dispatch time — :id, the owning aggregate's own identity_heads (real corpus proof — Account.Debit dispatched everywhere as number: ..., Account's own identified_by), AND Naming.reference_key(command.references) (real corpus proof — FreezeAccountsOnSuspension's for_each fan-out, whose own comment reads "account is the key the fan-out merges for each row it answers"). Both are simultaneously legal there, not context-dependent alternatives, so both are legal here : this mirrors that gate rather than re-deriving a narrower rule that would refuse one of two real, already-shipped dispatch conventions.

Returns:

  • (Boolean)


444
445
446
447
448
449
450
451
452
453
454
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 444

def self.command_declares?(command, field, aggregates, correlation_heads)
  return true if command.attributes.any? { |a| a.name == field }
  return true if field == :id
  return true if correlation_heads.include?(field)
  return false unless command.references

  referenced = aggregates.find { |a| a.hecks_name == command.references }
  return false unless referenced

  referenced.identity_heads.include?(field) || Naming.reference_key(command.references) == field
end

.command_lookup(aggregates) ⇒ Object



578
579
580
581
582
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 578

def self.command_lookup(aggregates)
  each_command(aggregates).each_with_object({}) do |(owner, command), index|
    index["#{owner}.#{command.hecks_name}"] = command
  end
end

.correlation_heads(process_managers) ⇒ Object

THE FOURTH addressing key ArgumentGate#refuse_unknown_arguments accepts, alongside :id/identity_heads/reference_key — every saga in THIS domain's own correlates_by head, carried through every dispatch as pure passthrough (Settlement's own comment: "reference: carries the correlation forward... this is pure passthrough, not an addressing key"). A command declaring none of its attributes named this is not a gap; the correlation key rides through commands that never read it, same as it does at runtime.



464
465
466
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 464

def self.correlation_heads(process_managers)
  process_managers.filter_map { |pm| pm.correlates_by && pm.correlation_head }
end

.correlation_key_violation(pm, aggregates) ⇒ Object



915
916
917
918
919
920
921
922
923
924
925
926
927
928
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 915

def self.correlation_key_violation(pm, aggregates)
  head, *rest = pm.correlates_by.to_s.split(".")
  events = reacted_events(pm)

  emitting_commands(events, aggregates).each do |owner, command|
    attribute = command.attributes.find { |a| a.name == head.to_sym }
    next unless attribute

    reason = list_or_scalar_violation(owner, attribute, rest)
    return reason if reason
  end

  nil
end

.each_command(aggregates) ⇒ Object

Every command this chapter declares, an aggregate's own AND every entity nested inside one, paired with a name for what declares it — shared by validate_event_shapes! and validate_with_projections!'s own command lookup, the same reach HecksagonBuilder#commands_in needs one level up (S8).



473
474
475
476
477
478
479
480
481
482
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 473

def self.each_command(aggregates)
  return enum_for(:each_command, aggregates) unless block_given?

  aggregates.each do |aggregate|
    aggregate.commands.each { |command| yield aggregate.hecks_name, command }
    aggregate.entities.each do |entity|
      entity.commands.each { |command| yield "#{aggregate.hecks_name}.#{entity.hecks_name}", command }
    end
  end
end

.emitting_commands(events, aggregates) ⇒ Object



938
939
940
941
942
943
944
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 938

def self.emitting_commands(events, aggregates)
  aggregates.flat_map do |aggregate|
    commands = aggregate.commands + aggregate.entities.flat_map(&:commands)
    commands.select { |command| (command.emits.map(&:to_s) & events).any? }
            .map { |command| [aggregate, command] }
  end
end

.event_emitters(aggregates) ⇒ Object

NOT MEMOISED — this used to be @event_emitters ||= on the builder instance, which is safe for a one-file chapter but wrong for one split across several: the FIRST file's build() call would compute and cache it from whatever @aggregates held at that moment, and every later file's own validation would keep reading that same stale snapshot, silently missing any command a later file adds. Recomputed fresh every call instead — this walks the whole chapter once per #build, not a hot path worth memoising at that cost.



493
494
495
496
497
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 493

def self.event_emitters(aggregates)
  each_command(aggregates).each_with_object(Hash.new { |h, k| h[k] = [] }) do |(owner, command), index|
    command.emits.each { |event_name| index[event_name] << [owner, command] }
  end
end

.event_identity_heads_for(event_name, aggregates) ⇒ Object

The identity heads of the aggregate that emits event_name — an entity's event is stamped with its OWNING aggregate's identity (Event#id is the parent's), so an owner spelled "Game.Knight" answers Game's heads.



559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 559

def self.event_identity_heads_for(event_name, aggregates)
  pairs = event_emitters(aggregates).fetch(event_name.to_s, [])
  return [] if pairs.empty?

  owner_name, = pairs.first
  aggregate = owner_aggregate(owner_name, aggregates)
  return [] unless aggregate

  heads = aggregate.identity_heads.map(&:to_sym)
  # AN ENTITY'S EVENT ALSO CARRIES THE PIECE'S OWN IDENTITY — the
  # args a piece was addressed by are the args its event announces
  # (`Emission#emit`: `payload: args`), so `id`-shaped heads are
  # genuinely there at runtime even though no `attribute` line on
  # the entity command declares them.
  entity_names = owner_name.to_s.split(".").drop(1)
  entity = entity_names.reduce(aggregate) { |owner, name| owner&.entities&.find { |e| e.hecks_name == name } }
  heads + (entity ? entity.identity_heads.map(&:to_sym) : [])
end

.event_shape(command, owner) ⇒ Object

STRUCTURAL, NOT NOMINAL. Two commands on two different aggregates that both emits "SameEvent" are free to type a field through two DIFFERENT, locally-scoped wrapper value objects (e.g. one aggregate's own value: SomeText vs another's value: OtherText, exactly the per-aggregate "own text VO" convention every aggregate in this grammar already follows for everything from RuleText to FieldRef) without actually disagreeing about the event's shape — comparing a.type by NAME would flag that as a violation for no real reason: an event is one fact, and two isomorphic wrapper types tell an identical one. So a value-object type is unwrapped to its OWN attribute shape (recursively — a wrapper could itself wrap another) before comparing, and only a primitive type (nothing left to unwrap) or two VOs that truly differ once unwrapped still counts as a real mismatch. owner carries the type's value_object lookup — a command's own attributes only know their type's NAME, never the aggregate that declared it, and two sibling aggregates in one chapter each keep a same-named VO private to themselves, so the unwrap has to ask the SAME aggregate the field's own command belongs to, never a neighbor's.



520
521
522
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 520

def self.event_shape(command, owner)
  command.attributes.map { |a| [a.name, unwrap_shape(owner, a.type.to_s), a.list?, a.optional?] }.sort
end

.event_shape_for(event_name, aggregates) ⇒ Object



547
548
549
550
551
552
553
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 547

def self.event_shape_for(event_name, aggregates)
  pairs = event_emitters(aggregates).fetch(event_name.to_s, [])
  return nil if pairs.empty?

  owner_name, command = pairs.first
  event_shape(command, owner_aggregate(owner_name, aggregates))
end

.find_reference_cycle(edges) ⇒ Object

Plain DFS with a visiting/done coloring, over the reference graph THIS chapter's own aggregates declare. Returns the ring itself (in the order it closes), or nil.



629
630
631
632
633
634
635
636
637
638
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 629

def self.find_reference_cycle(edges)
  state = {}

  edges.each_key do |start|
    cycle = reference_cycle_from(start, edges, state, [])
    return cycle if cycle
  end

  nil
end

.infer_hop_query_arguments!(bluebook) ⇒ Object

The chapter-wide half of AggregateBuilder's local query-argument inference. A hop cannot resolve while its aggregate is still being built; here every Reference has an owner and target, so a symbolic comparison can inherit the type of the scalar it compares without a duplicate query-local declaration.



703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 703

def self.infer_hop_query_arguments!(bluebook)
  bluebook.aggregates.each do |aggregate|
    aggregate.queries.each do |query|
      query.wheres.each do |clause|
        name = clause.value
        next unless name.is_a?(Symbol)
        next if query.attribute(name)
        next unless QuerySpecification::HopPath.hop_head?(clause.field, aggregate.attributes)

        plan = QuerySpecification::HopPath.plan(clause.field, aggregate.attributes)
        next if plan.refusal || plan.hops.empty?

        target = plan.hops.last.target
        head, *nested = plan.tail.to_s.split(".")
        leaf = if nested.empty? && target.lifecycle&.field.to_s == head
                 Attribute.new(name: name, type: String)
               else
                 root = target.attributes.find { |candidate| candidate.name.to_s == head }
                 found = root && QuerySpecification::FieldPath.leaf_attribute(root, nested) do |type|
                   target.value_object(type)
                 end
                 found && Attribute.new(name: name, type: found.type, list: found.list?)
               end
        next unless leaf

        query.attributes << leaf
      end
    end
  end
end

.list_or_scalar_violation(owner, attribute, segments) ⇒ Object



946
947
948
949
950
951
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 946

def self.list_or_scalar_violation(owner, attribute, segments)
  return "#{attribute.name} is a list — a correlation key must name one instance's own field, " \
         "not a whole collection" if attribute.list?

  walk_scalar(owner, attribute.type.to_s, segments)
end

.owner_aggregate(owner, aggregates) ⇒ Object

owner (from each_command) is a plain STRING — the aggregate's hecks_name alone, or "Aggregate.Entity" for an entity's own command. Either way the VALUE OBJECTS a command's fields can be typed with are the AGGREGATE's own (Entity carries no value_object lookup of its own — the whole rest of this file already resolves hop/type lookups only at the aggregate level, e.g. validate_hop_tail!'s target.value_object(type)), so only the first segment ever matters here.



543
544
545
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 543

def self.owner_aggregate(owner, aggregates)
  aggregates.find { |a| a.hecks_name == owner.to_s.split(".").first }
end

.projectable_scalar?(target, attribute) ⇒ Boolean

Returns:

  • (Boolean)


879
880
881
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 879

def self.projectable_scalar?(target, attribute)
  !attribute.list? && !attribute.reference? && target.value_object(attribute.type).nil?
end

.reacted_events(pm) ⇒ Object



930
931
932
933
934
935
936
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 930

def self.reacted_events(pm)
  ([pm.starts_on, pm.ends_on] + pm.handlers.map(&:event_type))
    .compact
    .reject { |event| event == ProcessManager::REFUSED }
    .map { |event| event.to_s.split("::").last }
    .uniq
end

.reference_cycle_from(node, edges, state, path) ⇒ Object



640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 640

def self.reference_cycle_from(node, edges, state, path)
  return nil if state[node] == :done
  return path[path.index(node)..] if state[node] == :visiting

  state[node] = :visiting
  path.push(node)

  edges[node].each do |target|
    next unless edges.key?(target) # a name this chapter never declares is dangling, not an edge

    found = reference_cycle_from(target, edges, state, path)
    return found if found
  end

  path.pop
  state[node] = :done
  nil
end

.refuse_entity_query_hops!(aggregate, entity) ⇒ Object



734
735
736
737
738
739
740
741
742
743
744
745
746
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 734

def self.refuse_entity_query_hops!(aggregate, entity)
  entity.queries.each do |query|
    query.wheres.each do |clause|
      next unless QuerySpecification::HopPath.hop_head?(clause.field, entity.attributes)

      raise Malformed,
            "#{aggregate.hecks_name}::#{entity.hecks_name}.#{query.hecks_name} asks about " \
            "#{clause.field}, which hops through #{entity.hecks_name}'s own reference — " \
            "an entity query does not follow a hop the way an aggregate's own does; ask " \
            "through the aggregate's own query instead, or open the target directly"
    end
  end
end

.unwrap_shape(owner, type_name, seen = []) ⇒ Object



524
525
526
527
528
529
530
531
532
533
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 524

def self.unwrap_shape(owner, type_name, seen = [])
  return type_name if owner.nil? # owner couldn't be resolved -- compare by name, same as before this unwrap existed
  return type_name if Attribute::PRIMITIVES.include?(type_name)
  return type_name if seen.include?(type_name) # a self-referential VO bottoms out on its own name, not an infinite unwrap

  shape = owner.value_object(type_name)
  return type_name unless shape # not this owner's own VO (a reference type, say) -- nothing further to unwrap

  shape.attributes.map { |a| [a.name, unwrap_shape(owner, a.type.to_s, seen + [type_name]), a.list?, a.optional?] }.sort
end

.validate_assembled!(bluebook) ⇒ Object

EVERY WHOLE-CHAPTER CHECK, IN ONE PLACE — the battery #build used to run inline, now a pure function of an assembled Bluebook::Chapter so MetaValidator.judge_deferred! can run it too, once, on a chapter whose files have ALL loaded (see #build's own comment for why that split exists at all). Public, not private_class_method'd, for exactly that second caller — MetaValidator needs to reach this with no builder instance in hand, only the chapter judge_deferred! already read back out of the registry.



237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 237

def self.validate_assembled!(bluebook)
  # moved to the language: an attribute type is a reference to its Shape,
  # so an undeclared value object fails reference resolution
  validate_reference_value_objects!(bluebook.aggregates)
  validate_correlation_keys!(bluebook.process_managers, bluebook.aggregates)
  validate_no_bidirectional_references!(bluebook.aggregates)
  unless MetaValidator.shadow_parsing?
    validate_event_shapes!(bluebook.aggregates)
    validate_with_projections!(bluebook.policies, bluebook.process_managers, bluebook.aggregates)
  end

  # Every hop AggregateBuilder#seal_query_field recognised and
  # deferred gets checked for real here — the earliest point a
  # hop CAN be checked, for exactly the reason
  # validate_no_bidirectional_references! above already gives:
  # `Bluebook.new` just stamped `hecks_owner` on every
  # aggregate, so `Reference#resolve` finally has a chapter to
  # walk. Before this line every target in the file (including
  # ones declared ABOVE the aggregate doing the asking) would
  # have resolved to nil.
  infer_hop_query_arguments!(bluebook)
  validate_query_hops!(bluebook)

  # Same precondition, same reason: a `projects` declaration's
  # own reference cannot resolve until every aggregate in the
  # chapter is real and owner-stamped (S12, ADR 0025).
  validate_projected_fields!(bluebook)
end

.validate_correlation_keys!(process_managers, aggregates) ⇒ Object

correlates_by NAMES A SCALAR, NOW CHECKED RATHER THAN TRUSTED.

ProcessManagerBuilder#validate! already refuses a bare, undotted spelling — a SYNTACTIC guarantee that the declaration cannot leave the question open. It cannot go further: a process manager is built in isolation, before this chapter's aggregates exist to check against. Here, with the whole document assembled, the dotted path is walked for real — against whichever command actually emits an event this process manager reacts to — so a path that still lands on a value object is refused before the runtime ever has to decide what a non-scalar correlation key even means: a saga keys off this value directly, and a value object carries no guaranteed-stable identity to key on the way a scalar does.

A command that does not declare the path's first segment at all is silently skipped, not refused — correlation has two other fallback tiers below the payload dig (a correlation stamp, then the emitting aggregate's own reference key; saga_interpreter/correlation.rb), so an absent field is not this check's business. Only a field that resolves, and resolves to something other than a scalar, is.



903
904
905
906
907
908
909
910
911
912
913
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 903

def self.validate_correlation_keys!(process_managers, aggregates)
  process_managers.each do |pm|
    next unless pm.correlates_by

    reason = correlation_key_violation(pm, aggregates)
    next unless reason

    raise ProcessManagerBuilder::InvalidProcessManager,
          "#{pm.name} correlates_by #{pm.correlates_by.inspect}, but #{reason}"
  end
end

.validate_event_shapes!(aggregates) ⇒ Object

EVENTS ARE FIRST-CLASS BY CONVENTION, NOT BY DECLARATION (ADR 0025, "events and reactions" — "a domain event is a value object with its own attributes, not a label"). No new event do ... end construct exists to hand-author and keep in step with every emitting command by hand — an event's own known shape IS whichever command(s) declare emits for its name, and this is the ONE thing that has to hold for that convention to mean anything: every command that emits a given name has to agree on what it carries. An event is one fact; a fact does not carry two different truths depending on who is telling it.

STRUCTURAL fields only (name/type/list/optional) — pattern:/ admits:/default: are refinements ON a field, not a second claim about what the payload holds, so two emitting commands are free to differ there without actually disagreeing about the event's own shape.



323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 323

def self.validate_event_shapes!(aggregates)
  event_emitters(aggregates).each do |event_name, pairs|
    next if pairs.size == 1

    shapes = pairs.map { |(owner, command)| event_shape(command, owner_aggregate(owner, aggregates)) }.uniq
    next if shapes.size == 1

    named = pairs.map { |(owner, command)| "#{owner}.#{command.hecks_name}" }.sort
    raise Malformed,
          "#{event_name.inspect} is emitted with different shapes by #{named.join(' and ')}" \
          "an event is one fact, and every command that emits it must declare the same fields"
  end
end

.validate_hop_clause!(aggregate, query, clause) ⇒ Object



748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 748

def self.validate_hop_clause!(aggregate, query, clause)
  plan = QuerySpecification::HopPath.plan(clause.field, aggregate.attributes)

  case plan.refusal
  when :unresolvable
    # HopPath.plan pushes even an unresolved hop onto `hops`
    # before reporting this, specifically so `target_name` —
    # real, known at declaration, independent of whether
    # `resolve` succeeded — is always here to name.
    raise Malformed,
          "#{aggregate.hecks_name}.#{query.hecks_name} asks about #{clause.field}, " \
          "which hops to #{plan.hops.last.target_name}, which this chapter never " \
          "declares — a hop into an aggregate this chapter cannot see resolves to " \
          "nothing, and a where that resolves to nothing matches nothing and " \
          "refuses nothing"
  when :too_deep
    raise Malformed,
          "#{aggregate.hecks_name}.#{query.hecks_name} asks about #{clause.field}, " \
          "whose hop chain reaches #{QuerySpecification::HopPath::MAX_HOPS} " \
          "references deep without landing — a chain this long is refused as a " \
          "likely mistake, not a structural limit"
  end

  target = plan.hops.last.target
  validate_hop_tail!(aggregate, query, clause, target, plan.tail)
end

.validate_hop_comparator!(aggregate, query, clause, target, attribute, nested) ⇒ Object

A WHERE hop with an ordered comparator is legitimate ("client whose balance > 500") — AggregateBuilder#seal_ordered_comparator already deferred this exact check for the same reason every other hop check is deferred, and this is where it gets asked, against the hop's TARGET instead of the querying aggregate.

Raises:



810
811
812
813
814
815
816
817
818
819
820
821
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 810

def self.validate_hop_comparator!(aggregate, query, clause, target, attribute, nested)
  return unless AggregateBuilder::ORDERED_COMPARATORS.include?(clause.op.to_s.to_sym)
  return if attribute &&
            QuerySpecification::FieldPath.numeric?(attribute, nested) { |type| target.value_object(type) }

  held = attribute ? "holds no number" : "is the lifecycle field, which holds text"
  raise Malformed,
        "#{aggregate.hecks_name}.#{query.hecks_name} compares #{clause.field} with " \
        "#{clause.op} after hopping to #{target.hecks_name}, but the field it lands " \
        "on #{held} — an ordered comparison needs a numeric field, and over " \
        "anything else the adapters answer differently or not at all"
end

.validate_hop_tail!(aggregate, query, clause, target, tail) ⇒ Object

The same three-way answer seal_query_field gives for its OWN aggregate's fields — landing on a real scalar (fine), landing on a value object (refused by name), or naming nothing at all (refused by name) — asked instead of the hop's TARGET aggregate, since that is whose shape the tail actually has to answer for.

Raises:



780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 780

def self.validate_hop_tail!(aggregate, query, clause, target, tail)
  name, *nested = tail.to_s.split(".")
  attribute = target.attributes.find { |candidate| candidate.name.to_s == name }
  return validate_hop_comparator!(aggregate, query, clause, target, attribute, nested) if
    nested.empty? && (attribute || target.lifecycle&.field.to_s == name)
  return validate_hop_comparator!(aggregate, query, clause, target, attribute, nested) if
    nested.any? && attribute &&
    QuerySpecification::FieldPath.scalar_leaf?(attribute, nested) { |type| target.value_object(type) }

  if nested.any? && attribute &&
     !QuerySpecification::FieldPath.leaf_attribute(attribute, nested) { |type| target.value_object(type) }.nil?
    raise Malformed,
          "#{aggregate.hecks_name}.#{query.hecks_name} asks about #{clause.field}, " \
          "which hops to #{target.hecks_name} and then asks about #{tail}, which " \
          "lands on a value object, not a scalar — a dotted query path ends on a " \
          "scalar member, or the engines answer it differently"
  end

  raise Malformed,
        "#{aggregate.hecks_name}.#{query.hecks_name} asks about #{clause.field}, " \
        "which hops to #{target.hecks_name} and then asks about #{tail}, which " \
        "#{target.hecks_name} never declares — a query over a field that does " \
        "not exist matches nothing and refuses nothing"
end

.validate_no_bidirectional_references!(aggregates) ⇒ Object

A REFERENCE RING IS NOT A MODELLING CHOICE, IT IS A MISSING ONE — a DDD aggregate is a consistency boundary precisely because something outside it can only ever point IN, by id, never the other way. A caller must be able to reason about one aggregate alone ; a ring back to where it started means no aggregate in it is a boundary anyone can reason about without the rest of the ring, and the whole ring is really one aggregate wearing several names.

Checked at the bluebook level, not inside AggregateBuilder itself, because seeing a cycle needs every end declared — an aggregate finishes building long before it can know whether some later aggregate in the same file points back at it.

ACYCLIC WITHIN A CHAPTER (ADR 0025, "References") — widened from the direct pair (A -> B -> A) this used to catch alone to any ring, however long (A -> B -> C -> A), the same DFS coloring a reference graph needs for any cycle. A cross-chapter reference is UNREACHABLE here rather than unchecked: Reference#resolve is scoped to its own chapter by construction, so a target this chapter never declares is a dangling name, not an edge — edges.key? below is what keeps the walk from ever leaving this chapter's own aggregates. Self-reference stays legal (parent.parent.name for a hierarchy is real and safe) — excluded the same way the direct-pair check already excluded it.

Raises:



610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 610

def self.validate_no_bidirectional_references!(aggregates)
  edges = aggregates.each_with_object({}) do |aggregate, index|
    index[aggregate.hecks_name] = aggregate.reference_targets.uniq.reject { |target| target == aggregate.hecks_name }
  end

  cycle = find_reference_cycle(edges)
  return unless cycle

  ring = "#{cycle.join(' -> ')} -> #{cycle.first}"
  raise Malformed,
        "reference cycle: #{ring} — an aggregate points at another by id, and a " \
        "ring back to where it started means no aggregate in it is a boundary " \
        "anyone can reason about alone ; break the ring, or let one side be found " \
        "through a query instead of a reference pointing back"
end

.validate_projected_field!(aggregate, field) ⇒ Object

Raises:



844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 844

def self.validate_projected_field!(aggregate, field)
  plan = QuerySpecification::HopPath.plan("#{field.reference}/#{field.remote_field}", aggregate.attributes)

  if plan.refusal == :unresolvable
    raise Malformed,
          "#{aggregate.hecks_name}.projects :#{field.name} reads through :#{field.reference}, " \
          "which hops to #{plan.hops.last.target_name}, which this chapter never declares — " \
          "a projection through an aggregate this chapter cannot see resolves to nothing"
  end

  target = plan.hops.last.target
  remote_attribute = target.attributes.find { |candidate| candidate.name.to_s == plan.tail }

  # THE WORKED EXAMPLE ITSELF (ADR 0025) reads through a
  # LIFECYCLE field — banking's Customer.status is `lifecycle
  # :status`, never a plain `attribute` — the same fallback
  # validate_hop_tail! already gives a query's own hop tail. A
  # lifecycle field is always a plain string by construction ;
  # nothing further to check once it matches by name.
  return if remote_attribute.nil? && target.lifecycle&.field.to_s == plan.tail

  unless remote_attribute
    raise Malformed,
          "#{aggregate.hecks_name}.projects :#{field.name} reads #{target.hecks_name}'s own " \
          "#{plan.tail.inspect}, which #{target.hecks_name} never declares"
  end

  return if projectable_scalar?(target, remote_attribute)

  raise Malformed,
        "#{aggregate.hecks_name}.projects :#{field.name} reads #{target.hecks_name}'s own " \
        "#{plan.tail.inspect}, which is not a scalar — a projected field copies a single " \
        "value, never a reference, a value object, or a list"
end

.validate_projected_fields!(bluebook) ⇒ Object

THE TARGET HALF of projects validation (S12, ADR 0025) — AggregateBuilder#seal_projected_fields already checked the LOCAL half at declare time (the reference names a real reference_to on THIS aggregate); this checks the reference actually resolves to a real aggregate in this chapter, and that aggregate really declares remote_field as a scalar.

Reuses QuerySpecification::HopPath rather than re-deriving hop resolution a second way — "reference/remote_field" is the same single-hop shape a query's own /-spelled hop resolves, even though projects's own DSL spelling is dotted (from: :"customer.status"): two constructs, two spellings, one resolution primitive. A single hop can never reach HopPath::MAX_HOPS, so :too_deep is structurally unreachable here and is not special-cased.



838
839
840
841
842
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 838

def self.validate_projected_fields!(bluebook)
  bluebook.aggregates.each do |aggregate|
    aggregate.projected_fields.each { |field| validate_projected_field!(aggregate, field) }
  end
end

.validate_query_hops!(bluebook) ⇒ Object

THE OTHER HALF OF A HOP — AggregateBuilder#seal_query_field recognised the HEAD of a dotted where-field that names one of its own references and deferred it here, unable to check further: it cannot yet resolve what the reference points AT. This runs once every aggregate exists in one chapter, so it can.

Only WHERE clauses ever reach here — a hop on ORDER BY is refused outright, immediately, back in seal_query_field itself (that answer never needed the target's shape).

AN ENTITY'S OWN QUERIES DID reach EntityBuilder#reference_to (added after this comment first claimed otherwise — S9, ADR 0025) without ever reaching HERE: tier-1 sealing (AggregateBuilder#query_surfaces) already recognises a hop on an entity's own field and DEFERS it exactly like an aggregate's, but nothing ever walked entity queries at tier 2 to check the deferral — a bad hop, or even a well-formed one, built silently and then matched nothing at runtime (QueryInterpreter#entity_rows reads an element's fields by literal hash key, never follows a reference). Refused outright here instead of taught to follow the hop for real: no corpus member needs an entity query to cross a reference yet, and a named refusal beats a runtime that resolves nothing while looking like it might.



684
685
686
687
688
689
690
691
692
693
694
695
696
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 684

def self.validate_query_hops!(bluebook)
  bluebook.aggregates.each do |aggregate|
    aggregate.queries.each do |query|
      query.wheres.each do |clause|
        next unless QuerySpecification::HopPath.hop_head?(clause.field, aggregate.attributes)

        validate_hop_clause!(aggregate, query, clause)
      end
    end

    aggregate.entities.each { |entity| refuse_entity_query_hops!(aggregate, entity) }
  end
end

.validate_reference_value_objects!(aggregates) ⇒ Object

AN ENTITY COMMAND MAY NOT NAME ITSELF AS ITS ROOT.

That is the whole of what is left here, and it needs saying plainly because the sentence this used to raise — "references must target aggregate heads" — was never what it checked.

CommandBuilder#reference_to sets references ONLY when the target's bare name equals the owner's ; anything else becomes a reference ATTRIBUTE. So on an aggregate command references is always a copy of that aggregate's own name, and looking it up in an index of aggregates is a TAUTOLOGY — that branch never refused anything and structurally could not. Verified across all eight golden chapters before deleting it.

On a PIECE's command the owner is the entity, and an entity is not a head, so what this actually refuses is reference_to <its own name> written inside entity do … end. A piece is reached THROUGH its aggregate ; a command on one addresses the aggregate, never the piece.

Reference ATTRIBUTES are the language's business now — offered as the head's own id and resolved as references, so Aggregate.Reference and Command.Reference refuse an undeclared head with no predicate at all.

Raises:



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 287

def self.validate_reference_value_objects!(aggregates)
  heads = aggregates.map(&:hecks_name)

  violations = aggregates.flat_map do |aggregate|
    aggregate.entities.flat_map do |entity|
      entity.commands.filter_map do |command|
        next unless command.references
        next if heads.include?(command.references.to_s)

        "#{aggregate.hecks_name}.#{entity.hecks_name}.#{command.hecks_name} names itself as its root"
      end
    end
  end

  return if violations.empty?

  raise Malformed,
        "an entity command is addressed through its aggregate; #{violations.uniq.join('; ')}"
end

.validate_with_projections!(policies, process_managers, aggregates) ⇒ Object

THE "EXPENSIVE HALF" the ADR names: "with: { account: :account } projecting into a reaction that has no declared contract ... breaks at dispatch rather than at load." Checked here, now that validate_event_shapes! (above) guarantees at most one real shape per event name, and command references being first-class (Naming.command_ref) means the TARGET side is a real resolvable command, not a string that might be a typo.

SAME-CHAPTER ONLY, ON PURPOSE — a with: whose source event or target command lives outside this chapter (an across policy reacting to another domain's event entirely) is silently left unchecked rather than refused: there is nothing here yet to check it against, and "unresolvable" is not the same claim as "wrong."

A FOR_EACH POLICY'S SOURCE ISN'T THE EVENT AT ALL — a fan-out with:'s symbols read the QUERY ROW for_each answers (FreezeAccountsOnSuspension's own comment: "account is the key the fan-out merges for each row"), which this has no shape for; the SOURCE half is skipped for those, the TARGET half (does the dispatched command actually declare the field) still runs, since that half is true regardless of where the value came from.



360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 360

def self.validate_with_projections!(policies, process_managers, aggregates)
  lookup = command_lookup(aggregates)
  heads  = correlation_heads(process_managers)

  policies.each do |policy|
    next if policy.with_spec.to_a.empty?

    source_event = policy.for_each.to_s.empty? ? policy.on_event : nil
    check_with_spec!(policy.trigger_command, source_event, policy.with_spec, lookup,
                     "#{policy.name}'s trigger", aggregates, heads)
  end

  process_managers.each do |pm|
    pm.handlers.each do |handler|
      handler.dispatches.each do |dispatch|
        next if dispatch.with_spec.to_a.empty?

        check_with_spec!(dispatch.command_name, handler.event_type, dispatch.with_spec, lookup,
                         "#{pm.name}'s dispatch #{dispatch.command_name}", aggregates, heads, pm: pm)
      end
    end
  end
end

.walk_scalar(owner, type_name, segments) ⇒ Object

Walks the remaining dotted segments through nested value objects. type_name starts as the head attribute's own declared type ; each step either bottoms out at a real scalar (nil — no violation) or names why it cannot: still a value object with no more path left, a value object this domain never declared, a field that value object does not have, or a segment left over after already reaching a scalar.



960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 960

def self.walk_scalar(owner, type_name, segments)
  if segments.empty?
    return nil if Attribute::PRIMITIVES.include?(type_name)

    return "#{type_name} is a value object, not a scalar — name one of its own fields, " \
           "e.g. #{type_name.downcase}.value"
  end

  return "#{type_name} is already a scalar — #{segments.join('.')} has nothing left to reach" if Attribute::PRIMITIVES.include?(type_name)

  shape = owner.value_object(type_name)
  return "#{type_name} is not a value object this domain declares" unless shape

  segment, *rest = segments
  attribute = shape.attributes.find { |a| a.name == segment.to_sym }
  return "#{type_name} has no field #{segment.inspect}" unless attribute
  return "#{type_name}.#{segment} is a list — a correlation key must name one instance's own field, " \
         "not a whole collection" if attribute.list?

  walk_scalar(owner, attribute.type.to_s, rest)
end

Instance Method Details

#aggregate_impl(name, &block) ⇒ Object

@chapter_named_givens is threaded into every aggregate this chapter builds — see AggregateBuilder#given's own comment for the sharing this enables; NOT a new top-level DSL word itself (an aggregate's own EXISTING given already both declares locally and write-throughs here as a side effect, the identical shape EntityBuilder#given's own write-through to its owner aggregate's pool already takes — no new spelling for "declare a precondition," one level wider, same word). RENAMED FROM aggregate — item #13's full metaprogrammed dispatch (slice 4c). Bootstrap-reachable (every core/attached chapter's own top-level shape is written with it), so also named in GenericDispatch::BOOTSTRAP_CALLS_FALLBACK.



87
88
89
90
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 87

def aggregate_impl(name, &block)
  @aggregates << AggregateBuilder.build(name, chapter_named_givens:   @chapter_named_givens,
                                              chapter_pending_givens: @chapter_pending_givens, &block)
end

#attaches_to_impl(*contexts) ⇒ Object

A SUB-LANGUAGE NAMES WHERE IT LANDS. ADR 0026's own seam: the core grammar does not name its extension points, so this chapter names ITSELF onto them instead — the core contexts (e.g. "Query", "ReadModel") whose own admitted words this chapter's Syntax aggregate contributes rows for. Variadic, and accumulating across calls the same reason identified_by/group_by are: nothing here requires one call to name every context at once. RENAMED FROM attaches_to — item #13's full metaprogrammed dispatch (slice 4c). Not bootstrap-reachable (only sub-language chapters like Paging use it; the CORE chapters never describe themselves with it).



69
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 69

def attaches_to_impl(*contexts) = (@attaches_to ||= []).concat(contexts.map(&:to_s))

#buildObject



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 121

def build
  # The chapter is the top of the construct chain — `Bluebook` is a
  # ROOT, and its constructor stamps every aggregate and read model with
  # itself as owner, so every `hecks_fqn` below resolves by walking up
  # to it. No constants are installed at load time : the public door is
  # a per-boot projection, installed by `Loader.bind_runtime` once a
  # dispatcher exists to close over (facade/surface.rb).
  bluebook = Bluebook::Chapter.new(name: @name, version: @version, vision: @vision,
                                   aggregates: @aggregates,
                                   read_models: @read_models,
                                   policies: @aggregates.flat_map(&:policies) + @policies,
                                   process_managers: @process_managers,
                                   classification: @classification,
                                   formerly_known_as: @formerly_known_as,
                                   attaches_to: @attaches_to || [])

  # SAME REASON, SAME GATE — a bare chapter-given may still be
  # pending (see `AggregateBuilder#pending_chapter_given`) if a
  # file that would resolve it hasn't loaded yet; resolving now
  # would see the same incomplete `@chapter_named_givens`
  # `validate_assembled!` below would. Deferred to
  # `MetaValidator.judge_deferred!` the same way, and BEFORE
  # `validate_assembled!` there — nothing downstream should ever
  # read an unresolved placeholder's fields.
  resolve_pending_chapter_givens! unless MetaValidator.deferring?

  # A CHAPTER MAY BE SPLIT ACROSS FILES (see `self.build`'s own
  # comment). Every check below needs the WHOLE chapter present —
  # a hop, a projection, a correlation key or an event shape can
  # equally name a construct declared in a file that has not
  # loaded yet, and `@aggregates`/`@process_managers` here are
  # only ever as complete as whatever has loaded SO FAR. So,
  # exactly like `MetaValidator.call` below, this is skipped
  # while `MetaValidator.defer` is loading the chapter's files
  # and run once instead — by `MetaValidator.judge_deferred!`,
  # against the fully assembled chapter — after the last one
  # loads. A single-file chapter (still the common case) never
  # sees `deferring?` true here at all, so its own checks still
  # run inline, exactly as before.
  self.class.validate_assembled!(bluebook) unless MetaValidator.deferring?

  # The language judges the bluebook, in the language. Last, so the
  # meta-domain sees a fully built IR — the whole-document rules need
  # every declaration present, which is why they cannot be givens fired
  # at declaration time.
  MetaValidator.call(bluebook)
end

#coreObject



71
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 71

def core       = @classification = :core

#formerly_known_as(value) ⇒ Object

A domain's own identity can change — this names what it used to be, so the storage layer can recognize its own history under the old name instead of minting a brand-new lineage from nothing.



56
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 56

def formerly_known_as(value) = @formerly_known_as = value.to_s

#genericObject



73
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 73

def generic    = @classification = :generic

#policy(name, &block) ⇒ Object



113
114
115
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 113

def policy(name, &block)
  @policies << PolicyBuilder.build(name, &block)
end

#process_manager(name, &block) ⇒ Object



117
118
119
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 117

def process_manager(name, &block)
  @process_managers << ProcessManagerBuilder.build(name, &block)
end

#read_model(name, &block) ⇒ Object

read_model is the word (ADR 0025 reverts report — the IR construct, the registry API, and the docs filename all said read_model the whole time; no era was ever minted under report, so this is history and source agreeing again). report stays answered under MetaValidator.shadow_parsing? (S0a's own bridge) for the same reason has_many does — frozen era text that used it must keep booting; live source refuses it, naming the replacement.



100
101
102
103
104
105
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 100

def read_model(name, &block)
  # A read model gathers heads from SEVERAL aggregates, so no single head
  # declares it — the chapter does. Its owner is stamped in `build`, where
  # the chapter namespace exists.
  @read_models << ReadModelBuilder.build(name, &block)
end

#report(name, &block) ⇒ Object

Raises:



107
108
109
110
111
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 107

def report(name, &block)
  return read_model(name, &block) if MetaValidator.shadow_parsing?

  raise Malformed, "report is gone — read_model is the word now"
end

#resolve_pending_chapter_givens!Object

THE OTHER HALF OF A CHAPTER-WIDE given REFERENCE — AggregateBuilder#pending_chapter_given recognised an unresolved bare reference and deferred it here, unable to check further: a later file in this SAME chapter might still declare the real thing. Runs once every file has loaded, against the now-complete @chapter_named_givens pool — the IDENTICAL lookup reference_named_chapter_given already does, just late enough to see every aggregate's own declarations, not only the ones loaded before the referencing one.

MUTATES each placeholder Given IN PLACE rather than replacing it — it is already embedded, by Ruby object reference, in the referencing aggregate's own preconditions and in any command (same aggregate) that separately bare-referenced the same description, so there is nothing downstream holding a second, now-stale copy to update. Instance-level (not self., unlike validate_assembled!) — unlike that battery, this needs @chapter_named_givens itself, which only exists on the builder instance still open for this chapter (MetaValidator.judge_deferred! reaches it via registry.bluebook_builder(name), guaranteed already present).



190
191
192
193
194
195
196
197
198
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 190

def resolve_pending_chapter_givens!
  @chapter_pending_givens.each do |entry|
    resolved = resolve_pending_chapter_given(entry)
    entry[:placeholder].description = resolved.description
    entry[:placeholder].canonical   = resolved.canonical
    entry[:placeholder].predicate   = resolved.predicate
  end
  @chapter_pending_givens.clear
end

#supportingObject



72
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 72

def supporting = @classification = :supporting

#vision(value) ⇒ Object



47
48
49
50
51
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 47

def vision(value)
  # moved to the language: Vision invariant, on Chapter.Declare

  @vision = value
end