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
32
33
34
35
36
37
38
# 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 = []
  # ONE LEVEL WIDER STILL — the CHAPTER-WIDE, ENTITY-SCOPED pool
  # (the piece analogue of `@chapter_named_givens`, above). See
  # `EntityBuilder#given_impl`'s own comment for what this
  # closes; `docs/implemented/resolution-rules/
  # chapter-entity-given.md` for the full algorithm.
  @chapter_entity_named_givens   = {}
  @chapter_entity_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.



1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 1066

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.



447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 447

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)


495
496
497
498
499
500
501
502
503
504
505
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 495

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



629
630
631
632
633
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 629

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.



515
516
517
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 515

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

.correlation_key_violation(pm, aggregates) ⇒ Object



984
985
986
987
988
989
990
991
992
993
994
995
996
997
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 984

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).



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

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



1007
1008
1009
1010
1011
1012
1013
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 1007

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.



544
545
546
547
548
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 544

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.



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

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.



571
572
573
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 571

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



598
599
600
601
602
603
604
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 598

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.



680
681
682
683
684
685
686
687
688
689
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 680

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.



754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 754

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



1015
1016
1017
1018
1019
1020
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 1015

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.



594
595
596
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 594

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)


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

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

.reacted_events(pm) ⇒ Object



999
1000
1001
1002
1003
1004
1005
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 999

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



691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 691

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



785
786
787
788
789
790
791
792
793
794
795
796
797
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 785

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



575
576
577
578
579
580
581
582
583
584
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 575

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.



288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 288

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.



972
973
974
975
976
977
978
979
980
981
982
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 972

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.



374
375
376
377
378
379
380
381
382
383
384
385
386
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 374

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



799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 799

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:



861
862
863
864
865
866
867
868
869
870
871
872
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 861

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:



831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 831

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:



661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 661

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:



895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 895

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

  # A PROJECTION MAY CHAIN THROUGH ANOTHER PROJECTION (S12, ADR
  # 0025's own boundary rule, followed through) — `target`'s
  # OWN projected fields live in `projected_fields`, a
  # separate list from `attributes`, so a match there is
  # invisible to the check above even though it names a real,
  # always-current, stored field. `Transfer.projects
  # :source_customer_status, from: :"source.customer_status"`
  # reads Account's own already-projected `customer_status`
  # this way — Account is one hop from Customer, Transfer is
  # one hop from Account, and neither aggregate needs to know
  # about the other's target two hops away. A projected
  # field's remote value is always a scalar by construction
  # (`RebuildSweep.remote_value` never copies a reference, a
  # value object, or a list), so nothing further to check once
  # it matches by name — same reasoning the lifecycle
  # fallback just above already applies.
  return if remote_attribute.nil? && target.projected_fields.any? { |f| f.name.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.



889
890
891
892
893
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 889

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.



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

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:



338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 338

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.



411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 411

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.



1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 1029

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.



94
95
96
97
98
99
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 94

def aggregate_impl(name, &block)
  @aggregates << AggregateBuilder.build(name, chapter_named_givens:          @chapter_named_givens,
                                              chapter_pending_givens:        @chapter_pending_givens,
                                              chapter_entity_named_givens:   @chapter_entity_named_givens,
                                              chapter_entity_pending_givens: @chapter_entity_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).



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

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

#buildObject



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
168
169
170
171
172
173
174
175
176
177
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 130

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?
  resolve_pending_chapter_entity_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



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

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.



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

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

#genericObject



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

def generic    = @classification = :generic

#policy(name, &block) ⇒ Object



122
123
124
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 122

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

#process_manager(name, &block) ⇒ Object



126
127
128
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 126

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.



109
110
111
112
113
114
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 109

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:



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

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_entity_givens!Object

THE ENTITY-SCOPED ANALOGUE, one level down — see #resolve_pending_chapter_givens!'s own comment; identical shape, resolved against @chapter_entity_named_givens instead.



241
242
243
244
245
246
247
248
249
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 241

def resolve_pending_chapter_entity_givens!
  @chapter_entity_pending_givens.each do |entry|
    resolved = resolve_pending_chapter_entity_given(entry)
    entry[:placeholder].description = resolved.description
    entry[:placeholder].canonical   = resolved.canonical
    entry[:placeholder].predicate   = resolved.predicate
  end
  @chapter_entity_pending_givens.clear
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).



200
201
202
203
204
205
206
207
208
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 200

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



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

def supporting = @classification = :supporting

#vision(value) ⇒ Object



54
55
56
57
58
# File 'lib/hecks/bluebook/dsl/bluebook_builder.rb', line 54

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

  @vision = value
end