Words available inside entity do ... end.
The tables on this page are generated from the language’s own
aggregate-local syntax tables (lib/hecks/language/**/*.bluebook)
by hecks language_run.project_reference — do not edit inside the markers. The prose
between them is hand-written and survives regeneration.
Seven of these nine run against examples/banking’s LedgerEntry — the
movements inside an Account, which have their own identity, their own
state machine and their own commands, but no life apart from the account
holding them. reference_to and nested entity appear on no entity
anywhere in the real corpus, so both get a small chapter of their own
further down.
```ruby boot Hecks::Adapters::Folder.new.load_bluebooks(File.join(InMemoryDomain::ROOT, “examples/banking/bluebook”))
Hecks.hecksagon(“Banking”) do attaches “Governance” Banking::Customer.persisted_by(“Memory”) Banking::Account.persisted_by(“Memory”) end Hecks.hecksagon(“Governance”) do Governance::RoleAssignment.persisted_by(“Memory”) Governance::RoleTransition.persisted_by(“Memory”) end
```ruby bluebook
Hecks.bluebook "EntityReference" do
vision "An entity that points at something outside the record holding it."
aggregate "Manifest" do
attribute :docket, Docket
identified_by :docket
attribute :crates, list_of(Crate)
value_object("Docket") { attribute :value, String }
value_object("Handler") { attribute :value, String }
value_object("Slot") { attribute :value, Integer }
value_object("Stamp") { attribute :value, String }
entity "Crate" do
attribute :slot, Slot
identified_by :slot
attribute :handler, Handler
# THE WORD THIS CHAPTER EXISTS FOR — a crate inside one manifest,
# naming the depot that holds it, which is its own aggregate.
reference_to Depot
# THE SAME MECHANISM, NAMED FOR ITS OWN CARDINALITY — has_many/
# has_one/belongs_to mint the identical Reference-typed attribute
# reference_to does (relationship_attribute, shared by all four
# words); only the shape and the required-by-default rule differ.
# `origin` reads exactly like reference_to's own `depot` above,
# just spelled to say WHY: the depot this crate started from,
# never optional. `destination` may not be decided yet.
# `waypoints` is the one shape reference_to cannot express at
# all — a LIST of depots, admitting zero.
belongs_to Depot, as: :origin
has_one Depot, as: :destination, optional: true
has_many Depot, as: :waypoints
attribute :seals, list_of(Seal)
command "Seal" do
attribute :stamp, Stamp
sets :seals, append: { stamp: :stamp }
emits "CrateSealed"
end
command "Route" do
attribute :destination, Depot, optional: true
attribute :waypoints, list_of(Depot)
sets :destination
sets :waypoints
emits "CrateRouted"
end
# THE WORD THE LAST SECTION OF THIS PAGE EXISTS FOR — a piece
# nested inside a piece: one inspection seal, inside one crate,
# inside one manifest.
entity "Seal" do
attribute :stamp, Stamp
identified_by :stamp
end
end
command "OpenManifest" do
sets :docket
emits "ManifestOpened"
end
command "AddCrate" do
reference_to Manifest
attribute :slot, Slot
attribute :handler, Handler
attribute :depot, Depot
attribute :origin, Depot
sets :crates, append: { slot: :slot, handler: :handler, depot: :depot, origin: :origin }
emits "CrateAdded"
end
end
aggregate "Depot" do
attribute :code, Code
identified_by :code
value_object("Code") { attribute :value, String }
command "OpenDepot" do
sets :code
emits "DepotOpened"
end
end
end
```ruby boot Hecks.hecksagon(“EntityReference”) do EntityReference::Manifest.persisted_by(“Memory”) EntityReference::Depot.persisted_by(“Memory”) end
```ruby
runtime.dispatch("Banking::Customer.Register", with: { reference: { value: "en-1" },
name: { given: "Evelyn", family: "Boyd" },
email: { address: "evelyn@example.com" } })
account = Banking::Account.open!(customer: "en-1", number: { value: "en-a1" },
kind: { name: "current" }, daily_limit: { cents: 50_000 })
account.credit!(amount: { cents: 10_000 }, narrative: { text: "opening" })
account.debit!(amount: { cents: 2_500 }, narrative: { text: "groceries" })
description description — fills description
| argument | kind | required | fills | |—|—|—|—| | positional 1 | text | true | description |
A free-text label for the entity — no rules attached, read by nothing but a human. Same word, same shape, as an aggregate’s own description.
Carried on the IR, not on any record — nothing at runtime reads it:
ledger_entry = runtime.registry.bluebook("Banking").aggregate("Account").entities.first
ledger_entry.description # => "One movement across the account, in the order it was posted."
identified_by identified_by, type, as: do ... end / identified_by identified_by, type, as: — opens a ValueObject body, fills identified_by
| argument | kind | required | fills |
|—|—|—|—|
| positional 1 | symbol | false | identified_by |
| positional 1 | constant | false | type |
| as: | symbol | false | name |
Names the field that tells one element of the list apart from another — unique within the parent, not globally, since a FoyerTicketNumber only has to be unambiguous inside its own counter. See entities.md for how this identity is carried alongside the parent’s own when a command or query reaches through the aggregate.
LedgerEntry is identified_by :sequence (with attribute :sequence,
LedgerSequence declared alongside it), and the sequence only has to
be unique inside its own account — every account starts counting at
one:
account.ledger.map { |entry| entry[:sequence][:value] } # => [1, 2]
Which is why reaching one takes BOTH identities: the parent’s, then the entity’s own.
given description, declared_by: do ... end — fills preconditions
| argument | kind | required | fills |
|—|—|—|—|
| positional 1 | text | true | description |
| declared_by: | text | false | declared_by |
The SAME word an aggregate declares (see aggregate.md’s own “given”),
one level down: a precondition shared across this entity’s OWN
commands, declared once — block required — and referenced back by
name, with no block of its own, from any command that needs it. The
resolved canonical predicate lands on the referencing command’s own
givens either way, so a command’s own rule enforcement is identical
whichever construct declared the wording.
LedgerEntry declares three — "entry is posted", "customer is
active", and "account is open" — and both Amend and Reverse read
them all back rather than retyping the parent.-qualified predicates
(hecks codemod_run.hoist_local_givens hoisted the third, “entry is
posted”, from the two commands’ own identical local declarations —
the exact same duplication this entity’s other two preconditions were
hand-hoisted for, round 4’s own motivating case):
ledger_entry = runtime.registry.bluebook("Banking").aggregate("Account")
.entities.find { |e| e.hecks_name == "LedgerEntry" }
ledger_entry.preconditions.map(&:description) # => ["entry is posted", "customer is active", "account is open"]
ledger_entry.commands.find { |c| c.hecks_name == "Amend" }.givens.map(&:description) # => ["customer is active", "account is open", "entry is posted", "an amendment leaves a non-negative amount"]
Same canonical text either way — a referencing command’s own given
carries the entity’s declared predicate, not a copy:
ledger_entry.commands.find { |c| c.hecks_name == "Reverse" }.givens.map(&:canonical) # => ["parent.customer_status == \"active\"", "parent.status == \"open\"", "state == \"posted\""]
given also SHARES CHAPTER-WIDE, ACROSS AGGREGATES — a bare
given(description), written directly inside a DIFFERENT aggregate’s
own entity, resolves against whichever piece anywhere in the same
chapter already declared that description, the identical move
aggregate.md’s own “given” makes one level up
(docs/implemented/resolution-rules/chapter-entity-given.md).
SafeDepositBox::Visit names Account::LedgerEntry’s own “customer is
active” back rather than retyping parent.customer_status == "active"
a second time — a genuinely different aggregate, the identical
canonical, since both pieces’ own owning aggregate carries the
identical-named projected field (projects :customer_status):
visit = runtime.registry.bluebook("Banking").aggregate("SafeDepositBox")
.entities.find { |e| e.hecks_name == "Visit" }
visit.preconditions.map { |g| [g.description, g.canonical] } # => [["customer is active", "parent.customer_status == \"active\""], ["box is rented", "parent.status == \"rented\""]]
declared_by: disambiguates the same way it does one level up — only
needed once the same description means a genuinely different predicate
somewhere else in the chapter’s own pieces; omitted here because
Account::LedgerEntry is currently the only piece in this chapter
declaring “customer is active” with a block.
invariant description do ... end — fills invariants
| argument | kind | required | fills | |—|—|—|—| | positional 1 | text | true | description |
The SAME word an aggregate declares (see aggregate.md’s own
“invariant”) and a value object declares (value_object.md’s own),
checked one level further down than either: not once against the
aggregate’s own flat state, and not once per VALUE OBJECT instance,
but against EVERY INSTANCE of this piece the aggregate holds — a
list_of field’s every element, checked the same two points
(after every mutation, before save) the aggregate’s own invariants
already run at (Admissibility#enforce_invariants’s own recursive
walk). No reference-by-name form, unlike given — no known corpus
need yet for one piece’s own invariant to be shared with a sibling
piece; given’s own cross-entity write-through is where that
capability would extend from if the need shows up.
Visit declares one — "a written note is not blank" — checked
against every visit SafeDepositBox holds, not just the one a
command just touched:
visit = runtime.registry.bluebook("Banking").aggregate("SafeDepositBox")
.entities.find { |e| e.hecks_name == "Visit" }
visit.invariants.map(&:description) # => ["a written note is not blank"]
Something a VALUE OBJECT invariant on VisitNote itself could not
express: note is OPTIONAL, so a VO invariant on it never even runs
when a visit carries none — correct for absence, but blind to
“present, and empty.” Only the OWNING piece, reading its own optional
field, can tell those two apart:
visit.invariants.first.canonical # => "!note || !note.text.to_s.empty?"
command name, from: do ... end — opens a Command body
| argument | kind | required | fills |
|—|—|—|—|
| positional 1 | text | true | name |
| from: | literal | false | from |
Same vocabulary as a command on an aggregate — see command.md — but this one never gets an entry point of its own: nothing installs a module for an entity, so it’s reached only as Aggregate.Entity.Command, never independently. It also never declares reference_to; the parent qualifier in the dotted call already supplies both identities.
Reverse is LedgerEntry’s, not Account’s — addressed through the
account that holds it, and naming which entry by its own sequence:
runtime.dispatch_flat("Banking::Account.LedgerEntry.Reverse", number: { value: "en-a1" },
sequence: { value: 2 }, narrative: { text: "posted in error" })
Banking::Account.find("en-a1").ledger[1][:state] # => "reversed"
The other entry is untouched — a command on one element is not a command on the list:
Banking::Account.find("en-a1").ledger[0][:state] # => "posted"
parentNot a grammar word — parent is a plain Ruby method
(Runtime::EntityInterpreter#parent), reachable only INSIDE a given/
ensures expression written on an entity’s own command. An entity has
no life apart from the aggregate holding it, so its own rules
routinely need to ask about THAT record, one level up — LedgerEntry’s
own Amend/Reverse both check the owning Account’s customer and
the account’s own lifecycle state before touching one entry:
reverse = runtime.registry.bluebook("Banking").aggregate("Account")
.entities.find { |e| e.hecks_name == "LedgerEntry" }
.commands.find { |c| c.hecks_name == "Reverse" }
reverse.givens.map(&:canonical) # => ["parent.customer_status == \"active\"", "parent.status == \"open\"", "state == \"posted\""]
Enforced, not decorative — a fresh account whose customer is
suspended refuses an entry-level command through exactly this
reading, before state == "posted" (the entry’s OWN field, no
parent. needed) is ever reached:
runtime.dispatch("Banking::Customer.Register", with: { reference: { value: "pa-1" },
name: { given: "Parent", family: "Reader" }, email: { address: "pa@example.com" } })
account = Banking::Account.open!(customer: "pa-1", number: { value: "pa-a1" },
kind: { name: "current" }, daily_limit: { cents: 50_000 })
account.credit!(amount: { cents: 1_000 }, narrative: { text: "opening deposit" })
runtime.dispatch_flat("Banking::Customer.Suspend", reference: "pa-1", standing: { value: "under review" })
runtime.dispatch_flat("Banking::Account.LedgerEntry.Reverse", number: { value: "pa-a1" }, sequence: { value: 1 }, narrative: { text: "reversing" }) # ~> GivenNotMet: customer is active
query name do ... end — opens a Query body
| argument | kind | required | fills | |—|—|—|—| | positional 1 | text | true | name |
Reached the same dotted way a command is — Aggregate.Entity.Query — and answers across every parent that has a matching element, each row stamped with which parent it came from.
Reversed reads entries, not accounts — and each row says which account
it came from, under that parent’s own reference key:
rows = runtime.query("Banking::Account.LedgerEntry.Reversed")
rows.size # => 1
rows.first[:account] # => "en-a1"
lifecycle state_field, default: do ... end — fills state_field
| argument | kind | required | fills |
|—|—|—|—|
| positional 1 | symbol | true | state_field |
| default: | literal | true | state_start |
Opens the same transition vocabulary an aggregate’s lifecycle does, checked against this entity’s own state field — a LifecycleRefused here names the entity, never the parent. See the Lifecycle context page.
LedgerEntry declares lifecycle :state, default: "posted", and the
entry above has already moved. Reversing it twice is refused against the
ENTRY’s state — the account is still perfectly open:
runtime.dispatch_flat("Banking::Account.LedgerEntry.Reverse", number: { value: "en-a1" }, sequence: { value: 2 }, narrative: { text: "again" }) # ~> GivenNotMet: entry is posted
Banking::Account.find("en-a1").status # => "open"
attribute name, type, default:, optional:, pattern:, matches:, admits:, one_of: — fills attributes
| argument | kind | required | fills |
|—|—|—|—|
| positional 1 | symbol | true | name |
| positional 2 | constant | true | type |
| default: | literal | false | default |
| optional: | flag | false | optional |
| pattern: | text | false | pattern |
| matches: | symbol | false | matches |
| admits: | text | false | admits |
| one_of: | list | false | one_of |
Declares a field on the entity, scalar or value object — same word, same modifiers, as an aggregate’s own attribute. See the Type and ValueObject context pages for what each type position and modifier does.
An entity’s fields are its own, and they are read off the element rather than off the parent:
Banking::Account.find("en-a1").ledger[0][:amount][:cents] # => 10000
Banking::Account.find("en-a1").ledger[0][:direction][:value] # => "credit"
reference_to type, as:, optional: — fills attributes
| argument | kind | required | fills |
|—|—|—|—|
| positional 1 | constant | true | type |
| as: | symbol | false | name |
| optional: | flag | false | optional |
Points an entity at a real ROOT, the same way an aggregate’s own reference_to does — a Card entity’s own assignee_id, pointing at a Team. Never at another entity: there’s no cross-piece addressing anywhere in this language to resolve one against, so this only ever reaches a head.
EntityReference’s Crate is an entity inside Manifest, pointing at a
Depot — a real root of its own, not another piece:
runtime.dispatch("EntityReference::Depot.OpenDepot", with: { code: { value: "dp-1" } })
runtime.dispatch("EntityReference::Depot.OpenDepot", with: { code: { value: "dp-2" } })
runtime.dispatch("EntityReference::Manifest.OpenManifest", with: { docket: { value: "mf-1" } })
runtime.dispatch_flat("EntityReference::Manifest.AddCrate", manifest: "mf-1", slot: { value: 1 },
handler: { value: "Ada" }, depot: "dp-1", origin: "dp-1")
The reference is stored on the crate itself, one field among its own:
EntityReference::Manifest.find("mf-1").crates.first[:depot] # => "dp-1"
has_many type, as: — fills attributes
| argument | kind | required | fills |
|—|—|—|—|
| positional 1 | constant | true | type |
| as: | symbol | false | name |
The one shape reference_to/has_one/belongs_to cannot express at all — a LIST of targets, admitting zero, never refused for being empty the way a required singular relationship is (validate_relationship_cardinality’s own early return on attribute.list?). waypoints is every depot this crate has passed through in transit, set here without ever touching destination (has_one’s own section, next) — a required list needs no partner value the way a required singular field would:
runtime.dispatch_flat("EntityReference::Manifest.Crate.Route", docket: { value: "mf-1" }, slot: { value: 1 },
waypoints: ["dp-1", "dp-2"])
EntityReference::Manifest.find("mf-1").crates.first[:waypoints] # => ["dp-1", "dp-2"]
has_one type, as:, optional: — fills attributes
| argument | kind | required | fills |
|—|—|—|—|
| positional 1 | constant | true | type |
| as: | symbol | false | name |
| optional: | flag | false | optional |
belongs_to with optional: true and a name read from the OTHER direction — one target, but not every crate has been assigned one yet. Route (above) left destination unset on purpose, to show the difference validate_relationship_cardinality (command_rules/references.rb) actually enforces: belongs_to’s own origin below would refuse a nil the same way a bare reference_to does; has_one’s optional: true is what a required relationship’s own refusal reads as when it is turned off:
EntityReference::Manifest.find("mf-1").crates.first[:destination] # => nil
belongs_to type, as:, optional: — fills attributes
| argument | kind | required | fills |
|—|—|—|—|
| positional 1 | constant | true | type |
| as: | symbol | false | name |
| optional: | flag | false | optional |
The same word an aggregate’s own belongs_to is (aggregate.md’s own section — SafeDepositBox’s real belongs_to Customer), one level in: mints the identical reference_to-shaped attribute, required by default, named for which side of the relationship a piece is on rather than for its mechanism. Crate’s own origin is the depot it started from — every crate has one from the moment AddCrate creates it, the same “required” rule reference_to’s own depot above is under:
EntityReference::Manifest.find("mf-1").crates.first[:origin] # => "dp-1"
entity name do ... end — opens a Entity body
| argument | kind | required | fills | |—|—|—|—| | positional 1 | text | true | name |
A piece nested inside a piece — the same word, opening the same body, one level further in than Manifest’s own entity "Crate" above. EntityReference’s Crate nests Seal: one inspection stamp, inside one crate, inside one manifest — “no life outside its Crate,” the same way Crate itself has no life outside Manifest.
A nested entity is created the same way any entity is: by its OWNER’s own append command, never by a creating verb of its own. Crate.Seal is Crate’s own command, and reaching it takes both outer identities — Manifest’s own docket, then Crate’s own slot — the same two-part reach command.md’s own reading through parent already uses one level down:
runtime.dispatch_flat("EntityReference::Manifest.Crate.Seal", docket: { value: "mf-1" }, slot: { value: 1 }, stamp: { value: "inspected-1" })
EntityReference::Manifest.find("mf-1").crates.first[:seals].map { |seal| seal[:stamp][:value] } # => ["inspected-1"]
A second seal appends beside the first — nested data, ordinary list semantics:
runtime.dispatch_flat("EntityReference::Manifest.Crate.Seal", docket: { value: "mf-1" }, slot: { value: 1 }, stamp: { value: "inspected-2" })
EntityReference::Manifest.find("mf-1").crates.first[:seals].size # => 2