Bare sets :field (no to:) imports the owning aggregate’s or entity’s own
attribute :field, Type when the command hasn’t declared one itself.
sets :field already meant “set :field to the argument named :field”
(the omittable-to: shorthand). The command still needed a separate
attribute :field, Type line, byte-identical to the aggregate’s own
declaration, purely to accept that argument at all. Real, pervasive corpus
evidence: Account.Open in examples/banking/bluebook/
declared attribute :number, AccountNumber / attribute :kind, AccountKind
/ attribute :daily_limit, DailyLimit, each copy-pasted verbatim from the
aggregate’s own declaration a few lines above, each immediately followed by
a matching bare sets.
Mirrors S10’s given-reference pattern one level
down: “a precondition shared across commands is declared once… a command
references it by name,” now for attributes instead of preconditions.
For each mutation in the command’s own declared mutations:
mutation.op is not set, skip it (see
implicit-append-fields.md for the append
case).mutation.source (as text) is not equal to mutation.target (as
text), skip it — this is not the bare, self-referential form.attribute named mutation.target,
skip it — an explicit local declaration always wins, never overwritten.mutation.target on the OWNER (the aggregate
or entity this command is declared on). If none exists, do nothing —
this is not an error at this stage (see “Known limitations”).optional?, and admits, not just
the same name.| Command body | Outcome |
|---|---|
sets :name (owner declares attribute :name, Name, command declares nothing) |
Imports attribute :name, Name |
attribute :name, Name then sets :name |
Unchanged — explicit local wins |
sets :name, to: :other_name |
Not touched — genuinely different source, mutation.source != mutation.target |
sets :accepted, to: false |
Not touched — to: is a literal value, not a field reference |
sets :amount, increment: :adjustment |
Not touched — not a bare set op at all (an increment mutation) |
sets :field where neither the command nor the owner declares :field |
Nothing is imported; the command’s own attribute list stays as-is (a downstream build-time gate, AggregateBuilder#seal_mutation_targets, refuses this separately — not this rule’s own job) |
Declaration order. The owner’s own attribute :field must already exist
by the time this command’s own resolution runs — a strictly one-pass,
top-to-bottom constraint, the same one given-reference and identified_by
already carry. Every real bluebook satisfies this for attribute/sets
(an aggregate or entity always declares its own fields before the commands
that act on them) — no known corpus violation for this specific rule (unlike
implicit-append-fields.md’s three real
violations one level down).
Hecks::Bluebook::DSL::CommandBuilder#resolve_bare_set!
(lib/hecks/bluebook/dsl/command_builder.rb) — the owner’s own
attributes are threaded in as owner_attributes:, set by
AggregateBuilder#command (passing its own attributes) and
EntityBuilder#command (same).
rust/parser/src/parse/command.rs, resolve_bare_set — owner_attributes:
&[ir::Attribute] threaded through parse_body from
rust/parser/src/parse/aggregate.rs and rust/parser/src/parse/entity.rs.
Verified byte-identical against the real corpus via
spec/parser_parity_spec.rb.