Class: Hecks::Bluebook::DSL::CommandBuilder
- Inherits:
-
Object
- Object
- Hecks::Bluebook::DSL::CommandBuilder
- Includes:
- AttributeCollector, RuleReference, WordGate
- Defined in:
- lib/hecks/bluebook/dsl/command_builder.rb
Constant Summary collapse
- GRAMMAR_CONTEXT =
"Command"- KWARG_TO_OP =
setsis the word;then_setis the spelling every existing bluebook was written under (Syntax::Keyword carries the rename aswas:), 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 toto:(copy this argument/field into the target), different word. TODO upstream via bin/evolve (migration plan task 7): decide whetherfrom:orto: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 reusearithmetic/arithmetic_value_objectat all; see MutationApplier#apply's own:clampbranch.remove:-- vendored addition, not (yet) upstream hecks (migration plan task 4): the list-removal counterpart toappend:(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 againstmutation.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 ofto:-- 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 intoto:itself rather than given its own mutation op -- semantically identical, same UNSET-sentinel discipline theto: falsefix already established (a positionalfalsemust read as "set to false," not "absent," same as the keyword form). Only applied whento:itself was NOT also given, so an explicitto:keyword always wins over a stray positional.setsis the word (ADR 0025 revertsthen_set— the grammar already declaredsets,was: "then_set", and 143 of 143 live call sites arethen_set, so this method was the one thing still backwards).to:is OMITTABLE when it would only repeat the target —sets :numberalone already meansto: :number— and the REDUNDANT explicit spelling is refused outright (principle 1, "one idea, one spelling":sets :number, to: :numbersays nothingsets :numberdoesn't).from:— a pure synonym forto:the language's own refusal message had already forgotten about — is gone; writeto:. 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 ownArgument#selectscolumn ("op=set","op=append", ...; whole-project table-unification survey, item #1), the same fieldrust/parser/src/keywords.rs'sArgumentRow. selectsalready 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
Constants included from RuleReference
RuleReference::BOOTSTRAP_FALLBACK
Class Method Summary collapse
Instance Method Summary collapse
- #build ⇒ Object
-
#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. -
#emits(event_name) ⇒ Object
No raise here.
-
#ensures(description, &predicate) ⇒ Object
The POSTCONDITION — a given for the far side of the mutations, evaluated against the settled record with
oldnaming the state as it stood before them:ensures("...") { old.balance.cents == balance.cents + amount.cents }. -
#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". - #goal(value) ⇒ Object
-
#initialize(name, owner: nil, from: nil, named_givens: {}, owner_attributes: [], owner_constructs: [], entity_shared_givens: {}) ⇒ CommandBuilder
constructor
A new instance of CommandBuilder.
-
#provenance_impl(from:) ⇒ Object
See AggregateBuilder#provenance's own comment — identical shape, one level down.
-
#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. -
#role_impl(value) ⇒ Object
A command carries ONE responsibility role — the language never declared an OR between two roles, so a second
rolecall would otherwise silently win while the first still looked declared, exactly the failure modereference_to's own duplicate guard (below) already exists to prevent for a command's root. -
#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). -
#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. -
#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.
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
#build ⇒ Object
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.
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.
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 |