hecks

Entity

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

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

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

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"

Reading through parent

Not 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

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

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

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

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

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

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

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

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