Class: Hecks::Bluebook::DSL::CommandBuilder

Inherits:
Object
  • Object
show all
Includes:
AttributeCollector, RuleReference, WordGate
Defined in:
lib/hecks/bluebook/dsl/command_builder.rb

Constant Summary collapse

GRAMMAR_CONTEXT =
"Command"
KWARG_TO_OP =

sets is the word; then_set is the spelling every existing bluebook was written under (Syntax::Keyword carries the rename as was:), and it stays answered here forever — a renamed word's old era keeps booting, which is the whole point of the rename column.

Vendored addition, not (yet) upstream hecks: then_set :target, from: :source_field (hecks_conception/miette, found live in body/doctor/bluebook/doctor.bluebook) -- semantically identical to to: (copy this argument/field into the target), different word. TODO upstream via bin/evolve (migration plan task 7): decide whether from: or to: becomes the canonical spelling.

Vendored addition, not (yet) upstream hecks (migration plan task 4, i106 in-DSL math): multiply:/clamp: -- per-tick organ math (miette's body/organs/bluebook: strength decays ×0.98, weight/strength clamp to [0, 1]) that used to be shell-side awk and moved into the bluebook itself. multiply: mirrors increment/decrement's shape exactly (a Numeric amount, applied by CommandRules::Arithmetic -- see that file's own comment on the matching Float-support widening this required). clamp: is a genuinely different shape -- its source is always a literal [min, max] pair, never an argument reference, and it bounds the CURRENT value rather than combining it with an amount -- so it does not reuse arithmetic/arithmetic_value_object at all; see MutationApplier#apply's own :clamp branch.

remove: -- vendored addition, not (yet) upstream hecks (migration plan task 4): the list-removal counterpart to append: (plan.bluebook's own RemoveDependency/DeactivateSprint commands: "the runtime list-remove primitive (then_set remove:) drops it from the list element-wise, with no read-modify-write -- so a concurrent Add can never be lost"). Matches an element by VALUE equality against mutation.source (resolved and Value-coerced the same way increment/decrement/multiply already coerce their own amount -- see MutationApplier#removed).

sets :field, true -- vendored addition, not (yet) upstream hecks (migration plan task 8): a bare positional literal instead of to: -- 14 occurrences across hecks_nursury (oceanography.bluebook/volcanology.bluebook and others), always a boolean shorthand (sets :deployed, true, never a string/number positional -- checked directly, zero non- boolean occurrences of the bare-positional-second-arg shape anywhere in the corpus). Folded into to: itself rather than given its own mutation op -- semantically identical, same UNSET-sentinel discipline the to: false fix already established (a positional false must read as "set to false," not "absent," same as the keyword form). Only applied when to: itself was NOT also given, so an explicit to: keyword always wins over a stray positional.

sets is the word (ADR 0025 reverts then_set — the grammar already declared sets, was: "then_set", and 143 of 143 live call sites are then_set, so this method was the one thing still backwards). to: is OMITTABLE when it would only repeat the target — sets :number alone already means to: :number — and the REDUNDANT explicit spelling is refused outright (principle 1, "one idea, one spelling": sets :number, to: :number says nothing sets :number doesn't). from: — a pure synonym for to: the language's own refusal message had already forgotten about — is gone; write to:. THE OP EACH KWARG SELECTS — spec/syntax_conformance_spec.rb's own "selects the same op..." check holds this constant to the self- hosted table's own Argument#selects column ("op=set", "op=append", ...; whole-project table-unification survey, item #1), the same field rust/parser/src/keywords.rs's ArgumentRow. selects already carries. to: is the one kwarg whose own name differs from the op it selects — every other kwarg selects the op of its own name.

{ to: :set, append: :append, increment: :increment, decrement: :decrement,
multiply: :multiply, clamp: :clamp, remove: :remove }.freeze

Constants included from WordGate

WordGate::NOT_ADMITTED

Constants included from RuleReference

RuleReference::BOOTSTRAP_FALLBACK

Class Method Summary collapse

Instance Method Summary collapse

Methods included from RuleReference

build_rule, lookup, resolve_hash_chain, resolve_owner_keyed, resolve_sibling_scan, verify_resolves_via!

Methods included from AttributeCollector

#attribute_impl, #attributes, #closed_sets, #list_of_impl, #one_of_impl

Constructor Details

#initialize(name, owner: nil, from: nil, named_givens: {}, owner_attributes: [], owner_constructs: [], entity_shared_givens: {}) ⇒ CommandBuilder

Returns a new instance of CommandBuilder.



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/hecks/bluebook/dsl/command_builder.rb', line 27

def initialize(name, owner: nil, from: nil, named_givens: {}, owner_attributes: [], owner_constructs: [],
               entity_shared_givens: {})
  @name              = name
  @owner             = owner
  @givens            = []
  @ensures           = []
  @mutations         = []
  @emits             = []
  @named_givens      = named_givens
  @owner_attributes  = owner_attributes
  @owner_constructs  = owner_constructs
  # THE AGGREGATE-WIDE cross-entity pool — see
  # `AggregateBuilder#entity`'s own comment and `EntityBuilder#
  # given`'s. Empty (never populated) for an AGGREGATE-owned
  # command, which already checks its own owner's `named_givens`
  # directly and has no siblings to reach across; real only for
  # an ENTITY-owned command's own bare reference.
  @entity_shared_givens = entity_shared_givens
  # NORMALIZED the exact same way `StateTransition#from` already
  # is — one state or several, a single spelling either way,
  # both read back through `Array(...)` at check time.
  @from = case from
          when Array then from.map(&:to_s)
          when nil   then nil
          else            from.to_s
          end
end

Dynamic Method Handling

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

Class Method Details

.build(name, owner: nil, from: nil, named_givens: {}, owner_attributes: [], owner_constructs: [], entity_shared_givens: {}, &block) ⇒ Object



441
442
443
444
445
446
447
448
# File 'lib/hecks/bluebook/dsl/command_builder.rb', line 441

def self.build(name, owner: nil, from: nil, named_givens: {}, owner_attributes: [], owner_constructs: [],
               entity_shared_givens: {}, &block)
  builder = new(name, owner: owner, from: from, named_givens: named_givens,
                owner_attributes: owner_attributes, owner_constructs: owner_constructs,
                entity_shared_givens: entity_shared_givens)
  builder.instance_eval(&block) if block
  builder.build
end

Instance Method Details

#buildObject



415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
# File 'lib/hecks/bluebook/dsl/command_builder.rb', line 415

def build
  resolve_implicit_attributes!

  delegation = @mutations.find { |mutation| mutation.op == :delegate }
  if delegation && (@mutations.size > 1 || @emits.any?)
    raise Malformed,
          "#{@name} both delegates_to #{delegation.target} and declares its own " \
          "sets/emits — a delegating command is a pure passthrough (see delegates_to's " \
          "own comment); its result is the delegated command's own"
  end

  Command.declare(
    name:       @name,
    role:       @role,
    goal:       @goal,
    attributes: attributes,
    givens:     @givens,
    ensures:    @ensures,
    mutations:  @mutations,
    emits:      @emits,
    references: @references,
    from:       @from,
    provenance: @provenance
  )
end

#delegates_to_impl(target, with: {}) ⇒ Object

THE SYNCHRONOUS COUSIN OF trigger — an AGGREGATE-level command that hands its own dispatch to ONE nested entity command, checked and applied within the SAME atomic dispatch rather than a second one. Built because trigger/saga's own dispatch (Dispatcher #reenter) is a REACTION — the triggering command has already committed by the time it runs, and both PolicyInterpreter#deliver and SagaInterpreter#deliver_saga_dispatch rescue a target's own refusal and RECORD it rather than raising it back to the original caller. That is correct for what those two exist for (an eventually-consistent process that can compensate), and wrong for a caller who needs a synchronous yes/no on whether the thing they asked for actually happened — a chess move's own legality, for instance, checked live building domain/chess in a downstream project. delegates_to fills exactly that gap: the target entity command's own given/ensures are enforced as real, unrescued Ruby exceptions, so a refusal deep in the entity's own rules is the DELEGATING command's own refusal too, and nothing from either side is saved unless both sides pass.

target is always ONE hop, "Entity.Command" — an aggregate names the entity it owns directly, same reach a bare given reference already has (see Knight's own comment on this domain's shared givens), not a multi-segment dispatch chain. with: resolves the SAME way sets ..., append: {...}'s own field map and a policy's own trigger ..., with: {...} already do: each value names one of THIS command's own declared/implicit arguments, read at dispatch time and handed to the target under its own key.

MUTUALLY EXCLUSIVE with sets/emits on the SAME command — a delegating command is a pure passthrough by design (see this method's own header), so it declares no OTHER mutation or event of its own; its result IS whatever the delegated entity command's own sets/emits produced. Enforced in build, once every builder call has already run, so declaration order does not matter.

STORED AS A MUTATION, not a new Command field — a real, deliberate choice, not a shortcut. Command's own shape (givens/ensures/ mutations/emits/...) is not just Ruby: it round-trips through this language's OWN self-hosted meta-domain (Bluebook::MetaValidator dispatches every declaration into a "Bluebook" domain describing itself, then REBUILDS the real runtime graph from what THAT domain holds — Hecks.bluebook registers what MetaValidator.call returns, never the builder's own object graph directly, confirmed by reading meta_validator.rb's own self.call/self.hold). A genuinely NEW top-level Command field needs the meta-domain's own grammar (language/bluebook/behavior.bluebook or wherever Verb.Rule/Ensure/Change live) taught to carry it too — the same scale of change as the real "item #13" migration this file's own comments document throughout. A NEW MUTATION OP does not: sets's own mutations: field is ALREADY a fully round-tripped part of that contract (Assembly::CONTRACTS["Command"].fields[:mutations]), and an append-shaped mutation ALREADY carries a multi-key fields: hash the exact shape with: needs — so delegates_to rides that existing, already-correct wire format under a new op: :delegate instead of inventing a parallel one. MutationOp's own closed set (vocabulary.bluebook) gained "delegate" alongside "append" for exactly this reason, and the THREE meta-domain touch points that hard-coded op == "append" for the multi-binding shape (meta_validator/readings.rb#mutation_rows, meta_validator/shapes.rb#mutation, assembly/marks.rb#mutation) now check for :delegate alongside it, each with a comment pointing back here. RENAMED FROM delegates_to to delegates_to_impl on declaration — matches sets_impl/given_impl/reference_to_impl's own convention (language/bluebook/syntax.bluebook's own Keyword row for this word names calls: "delegates_to_impl"), the same word-vs-_impl split every hand-written (not yet item-#13- generic-dispatch-migrated) DSL word here already follows.



404
405
406
407
408
409
410
411
412
413
# File 'lib/hecks/bluebook/dsl/command_builder.rb', line 404

def delegates_to_impl(target, with: {})
  entity_name, _dot, command_name = target.to_s.rpartition(".")
  if entity_name.empty? || command_name.empty?
    raise Malformed,
          "#{@name}'s delegates_to #{target.inspect} does not name an entity and a command " \
          "(\"Entity.Command\") — the same one-hop shape a bare given reference already uses"
  end

  @mutations << Mutation.new(target: target.to_s, op: :delegate, source: with)
end

#emits(event_name) ⇒ Object

No raise here. "an event is named" is declared in the language itself — language/bluebook/behavior.bluebook, on Command.Announce — and MetaValidator is what enforces it. This is the first rule to move ACROSS rather than be duplicated : delete the declaration and an unnamed event is accepted, which is what makes the meta-domain load-bearing rather than decorative.



322
323
324
# File 'lib/hecks/bluebook/dsl/command_builder.rb', line 322

def emits(event_name)
  @emits << event_name.to_s
end

#ensures(description, &predicate) ⇒ Object

The POSTCONDITION — a given for the far side of the mutations, evaluated against the settled record with old naming the state as it stood before them: ensures("...") { old.balance.cents == balance.cents + amount.cents }. Same extraction, same Rule shape, same refusal form; EnsuresNotMet instead of GivenNotMet.



180
181
182
183
# File 'lib/hecks/bluebook/dsl/command_builder.rb', line 180

def ensures(description, &predicate)
  @ensures << build_rule(Given, description, predicate, owner_name: @name, word: "ensures",
                          extraction_failure: "a postcondition is carried as text, and this one has none")
end

#given_impl(description, &predicate) ⇒ Object

NO BLOCK is a REFERENCE, not a fresh declaration (S10, ADR 0025 — "a precondition shared across commands is declared once. An aggregate declares it by name and commands reference it"): the SAME word, the SAME shape (AggregateBuilder#given, block required there), so naming a precondition back is spelled exactly like declaring one would be, minus the block — one idea, one word, never a second spelling ("requires"/"precondition") for "use the one already named". Resolved against whatever the OWNING aggregate has declared so far — see AggregateBuilder#command's own comment on why that means declaration order matters here. RENAMED FROM given — item #13's full metaprogrammed dispatch (slice 4b), same reasoning as reference_to_impl above.



140
141
142
143
144
145
146
147
# File 'lib/hecks/bluebook/dsl/command_builder.rb', line 140

def given_impl(description, &predicate)
  return reference_named_given(description) unless predicate

  # moved to the language: given "a rule says what it means", on Verb.Rule

  @givens << build_rule(Given, description, predicate, owner_name: @name, word: "given",
                         extraction_failure: "its source could not be read, so no other runtime could ever evaluate it")
end

#goal(value) ⇒ Object



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

def goal(value) = @goal = value

#provenance_impl(from:) ⇒ Object

See AggregateBuilder#provenance's own comment — identical shape, one level down. RENAMED FROM provenance — item #13's full metaprogrammed dispatch (slice 4c). Bootstrap-reachable, in GenericDispatch::BOOTSTRAP_CALLS_FALLBACK.



85
# File 'lib/hecks/bluebook/dsl/command_builder.rb', line 85

def provenance_impl(from:) = @provenance = from

#reference_to_impl(type, as: nil, optional: false) ⇒ Object

optional: rides here as well as on a plain attribute : as: makes a reference into a NAMED ARGUMENT, and a named argument is exactly the kind of fact that may or may not be given. The meta-domain's Verb.Declare points at the Entity a command belongs to — and most commands belong to no entity at all. RENAMED FROM reference_to — item #13's full metaprogrammed dispatch (slice 4b). Bootstrap-reachable, in GenericDispatch::BOOTSTRAP_CALLS_FALLBACK.



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/hecks/bluebook/dsl/command_builder.rb', line 95

def reference_to_impl(type, as: nil, optional: false)
  demodulised = Naming.demodulise(type)
  # moved to the language: given "a command names what it acts on", on Verb.ActsOn

  # `as:` MEANS "a named attribute", not "the root I act on" — so a command
  # can point at another instance of its OWN kind. Without this,
  # `reference_to Aggregate, as: :points_at` on a command owned by Aggregate
  # read as a second self-reference and was refused as naming two roots,
  # which is how the meta-domain's own Aggregate.Reference could not say the
  # one thing it exists to say. Transfer has said
  # `reference_to Account, as: :source` for as long as banking has existed;
  # this is the same sentence when the target happens to be the owner.
  return cross_reference(demodulised, as, optional) if as || demodulised.to_s != @owner.to_s

  if @references
    raise Malformed,
          "#{@name} references #{@owner} twice — a command acts on ONE " \
          "root ; the second would silently win and the first would " \
          "still look declared"
  end

  @references = demodulised
end

#role_impl(value) ⇒ Object

A command carries ONE responsibility role — the language never declared an OR between two roles, so a second role call would otherwise silently win while the first still looked declared, exactly the failure mode reference_to's own duplicate guard (below) already exists to prevent for a command's root.

RENAMED FROM role — item #13's full metaprogrammed dispatch (slice 4). This is a uniqueness gate on PRIOR STATE (@role already set), not a pure function of the argument's own value — a genuinely different shape than a plain fill, so it stays hand-written and is reached through calls: like attribute was in slice 3. Bootstrap-reachable (every self-hosted command declares a role), so also named in GenericDispatch::BOOTSTRAP_CALLS_FALLBACK.

Raises:



69
70
71
72
73
74
75
76
# File 'lib/hecks/bluebook/dsl/command_builder.rb', line 69

def role_impl(value)
  raise Malformed,
        "#{@name} declares role twice — a command carries ONE " \
        "responsibility; the second would silently win and the " \
        "first would still look declared" if @role

  @role = value
end

#sets_impl(target, positional_to = UNSET, to: UNSET, append: UNSET, increment: UNSET, decrement: UNSET, multiply: UNSET, clamp: UNSET, remove: UNSET) ⇒ Object

RENAMED FROM sets — item #13's full metaprogrammed dispatch (slice 4c). The KWARG_TO_OP op-selection mapping is already table-verified (Argument#selects), but the REST (UNSET- sentinel discipline, redundant-spelling refusal, omittable- to: fallback, one-mutation-only refusal, the position- preserving resolve_*! reinsertion) is keyed off RUNTIME STATE, not a pure function of a static row — stays hand- written, reached through calls: like everything else here. Bootstrap-reachable, in BOOTSTRAP_CALLS_FALLBACK.



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/hecks/bluebook/dsl/command_builder.rb', line 267

def sets_impl(target, positional_to = UNSET, to: UNSET, append: UNSET,
              increment: UNSET, decrement: UNSET, multiply: UNSET, clamp: UNSET, remove: UNSET)
  # moved to the language: given "a mutation names a target", on Verb.Change

  to = positional_to if to.equal?(UNSET) && !positional_to.equal?(UNSET)

  # `to:` only ever REPEATS the target when it's a Symbol naming a
  # field — a literal (`to: false`, the bare positional-boolean
  # shorthand, a String, ...) is a VALUE, never a redundant name,
  # so it never has `.to_sym` to compare in the first place.
  if to.is_a?(Symbol) && to == target.to_sym
    raise Malformed,
          "#{@name}'s sets :#{target}, to: :#{target} repeats the target — " \
          "sets :#{target} alone already means the same"
  end

  given = { to: to, append: append, increment: increment, decrement: decrement,
            multiply: multiply, clamp: clamp, remove: remove }
          .reject { |_, source| source.equal?(UNSET) }
  named = given.to_h { |kwarg, source| [KWARG_TO_OP.fetch(kwarg), source] }

  # THE OMITTABLE CASE. No operation was named at all — not even a
  # bare `to:` — so this is `sets :field` alone, which means
  # exactly what the redundant, refused spelling above would have.
  named = { set: target } if named.empty?

  if named.size > 1
    raise Malformed,
          "#{@name}'s sets :#{target} tries to #{named.keys.join(' and ')} " \
          "at once — one mutation, one meaning"
  end

  op, source = named.first
  @mutations << Mutation.new(target: target.to_sym, op: op, source: source)
end

#state(name) ⇒ Object

THE RECORD'S OWN VALUE AS A MUTATION SOURCE — sets :positions, append: { ply: state(:ply), knights: state(:knights) } copies what the record holds NOW into the new element; sets :last, to: state(:current) copies one field onto another. A bare Symbol always names an argument (see resolve_append_fields!), so without this a command could not snapshot its own state at all. Literal::StateRef's own comment has the wire spelling.



333
# File 'lib/hecks/bluebook/dsl/command_builder.rb', line 333

def state(name) = StateRef.new(name.to_sym)

#then_set_impl(target, positional_to = UNSET, **kwargs) ⇒ Object

LEGACY UNDER SHADOW-PARSING (S0a's own bridge) — frozen era text minted before this rename still parses; live source refuses it, naming the replacement. RENAMED FROM then_set — item #13's full metaprogrammed dispatch (slice 5). Not bootstrap-reachable. Now has its own dedicated, status: "deprecated" Keyword row (syntax.bluebook) rather than living only as sets's own was: — see that row's own comment for why.

Raises:



311
312
313
314
315
# File 'lib/hecks/bluebook/dsl/command_builder.rb', line 311

def then_set_impl(target, positional_to = UNSET, **kwargs)
  return legacy_then_set(target, positional_to, **kwargs) if MetaValidator.shadow_parsing?

  raise Malformed, "#{@name}'s then_set is gone — sets is the word now"
end